一、线程概念
线程是操作系统中能进行运算调度的最小单位,被包含于进程之中,一个进程可能有多个线程,多个线程可以并行执行任务。
同一进程的不同线程共享进程的内存空间,当然,每隔线程也拥有自己独有的内存地址范围,其他线程不能访问。
线程状态
- 新建(NEW):线程刚创建,未start之前
- 可执行(RUNABLE):又分为Ready和Running。runable不一定是真的在运行, 也有可能是在等待CPU资源
- 阻塞(BLOCKED):一般是线程等待获取锁,来继续执行下一步的操作
- 无限期等待(WAITING):调用以下方法后出现。
Object.wait()
Thread.join()
LockSupport.park() - 限期等待(TIMED_WAITING):等待一段时间之后,会唤醒线程去重新获取锁。调用以下方法后出现。Thread.sleep(long)
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos()
LockSupport.parkUntil() - 死亡(TERMINATED):线程结束任务之后自己结束,或者产生了异常而结束
图片来源:https://i-blog.csdnimg.cn/blog_migrate/dbb18718338a38c41862b30191591d89.png
二、线程的使用
1.创建方式
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类
2.实现方式比较
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口
- 实现的线程类只要求可执行,继承整个Thread类开销过大
三、常用方法
1.wait()和sleep()
二者均会使线程处于阻塞状态,
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会
2.notify()和notifyAll()
wait()、notify()、notifyAll()只能用在同步方法或者同步控制块中使用,否则会在运行时抛出
- notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
- notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行
java.util.concurrent 类库中Condition类的 await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
3.yield()
静态方法 Thread.yield() 表明当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
4.join()
在当前线程 a 中调用另一个线程 b 的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。能够保证 a 线程的输出先于 b 线程的输出。
5.中断方法interrupt()
调用一个线程的 interrupt() ,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
- 如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
获取中断标记 interrupted()
interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
Executor 的中断操作shutdown() 、shutdownNow()、cancel(true)
- shutdown():等待线程都执行完毕之后再关闭
- shutdownNow():相当于调用每个线程的 interrupt() 方法
- cancel(true):只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程
四、线程间的协作
1.notify()
2.wait() notify() notifyAll()
3.await() signal() signalAll()
五、线程安全
1.使用不可变变量
不可变的类型:
- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
- Collections.unmodifiableXXX() 方法来获取一个不可变的集合。对集合进行修改的方法都直接抛出异常。
2.互斥同步(synchronized、ReentrantLock)
synchronized:
- 修饰对象、同步代码块:作用与该类型同一个对象,不同对象不影响
- 修饰方法、同步方法:作用于同一个对象
- 修饰类:作用于整个类。两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步
- 同步静态方法:作用域整个类
ReentrantLock: (J.U.C)包中的锁
- 简单使用:Lock lock = new ReentrantLock(); lock.lock(); lock.unlock();
- 公平锁实现:谁等的时间最长,谁就先获取锁 Lock lock = new ReentrantLock( true );
- 非公平锁实现:随机的获取,cpu时间片轮到哪个线程,哪个线程就能获取锁。默认是非公平锁。
Lock lock = new ReentrantLock( false ); - 可中断锁获取方式:lock.lockinterruptibly(); //等待锁的过程中会立即响应中断
比较:
1.锁的实现
synchronized是jvm实现的。ReentrantLock是JDK实现的。
2.性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同
3.等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4.公平锁
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但也可以是公平的。
5.锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择:
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,
JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。
使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
3.非阻塞同步
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。
3.1 CAS
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。
CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。
当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
3.2 AtomicInteger
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作
4.无需同步方案情形
如果一个方法本来就不涉及线程共享数据,自然就无须任何同步措施去保证正确性。
4.1 局部变量(栈封闭)
局部变量存储在虚拟机栈中,属于线程私有的
4.2 ThreadLocal 线程本地存储
不同线程调用时,只会修改自身所存储的数据。不影响其他线程中响应threadLocal的存储内容
5.集合线程安全
ArrayList、HashSet、HashMap等集合在进行并发修改时,会出现线程安全问题。
三种解决方案:
- Vector,jdk1.0出现的解决方案,采用synchronized关键词,效率低
· add、remove、get方法均加锁 - Collections.synchronizedList(),synchronizedSet(),synchronizedMap()等
· 锁集合对象本身
· 仅在add、remove中加锁,get未加锁
· 不能保证iterator循环获取时的线程安全,需额外加锁 - CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap;
· JUC包中的解决方案
· 写时复制技术,修改时lock加锁,先拷贝一份进行修改,再将集合引用指向新的集合
六、J.U.C - AQS
大大提高了并发性能
1.CountDownLatch
用来控制一个或者多个线程等待多个线程。
内部维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒
2.CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
3.Semaphore
类似于操作系统中的信号量,可以控制对互斥资源的访问线程数
七、J.U.C - 其它组件
1. FutureTask
FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。
2.BlockingQueue
接口有以下阻塞队列的实现:
FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
优先级队列 :PriorityBlockingQueue
延时队列:DelayQueue
SynchronousQueue:无缓冲,公平模式和非公平模式两种
(1)抛异常
add() :放入队列失败,抛异常
remove():取失败,抛异常
(2)特殊值
offer(anObject):可以放入队列,则返回true,否则返回false.
poll():取不到,返回null
(3)延时
offer(E o, long timeout, TimeUnit unit): 队列已满,等待直到超时
poll(long timeout, TimeUnit unit): 出队一个元素,如果存在则直接出队,如果没有空间则等待timeout时间,无元素则返回null
(4)阻塞
如果队列为空 take() 将阻塞,直到队列中有内容;
如果队列为满 put() 将阻塞,直到队列有空闲位置
3.ForkJoin
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算
八、volatile
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。