线程共享
每个线程运行时,都会有各自的栈空间,如果线程仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java 支持多线程共同访问同一对象,但为了避免线程共享带来的数据安全性问题,引入了锁机制。
1、synchronized 内置锁:
synchronized 修饰对象、方法区或代码块,可以使多线程在此处串行执行,保证多线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
对象锁:用于对象实例或者对象实例方法上,类的对象实例有多个,不同对象实例的对象锁互不影响。
类锁:用于类的静态方法或类的Class对象上,类的Class对象只有一个,所以每个类只有一个类锁。
实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁也是互不影响的。
2、volatile,最轻量的同步机制:
volatile只保证数据的可见性,不保证数据的原子性。多线程中,一个线程修改了volatile修饰的变量的值,其他线程会立即察觉到该变量的最新值。
最适合多线程一(个线程)写多(个线程)读的情况。
3、ThreadLocal:
ThreadLocal为每个Thread线程创建了一个对应的副本,每个线程操作的都是各自副本中的数据,这样就隔离了各线程对数据的共享,保证了数据的安全。
ThreadLocal的实质就是在Thread中有一个ThreadLocalMap,ThreadLocalMap中存在Entry<K, V>数组,可以添加多个Entry<K, V>数据结构,其中K为ThreadLocal的对象,只是保存的是该对象实例的弱引用(为了减少内存泄漏)。如下图:

ThreadLocal应用场景:
Spring事务就借助了ThreadLocal来实现。我们在Service里面往往会调用一系列DAO进行操作,要保证事务的原子性,需要每个DAO使用同一个数据库链接。
如果不使用ThreadLocal,就需要给每个DAO传入一个相同的数据源连接对象,这样无论是将数据库链接在DAO实例化作为构造参数,还是作为DAO的实例方法的参数,使用起来都特别不优雅。
而WEB容器中每个完整的请求周期都是由一个Thread来完成的,那么只要将这个数据库链接放入ThreadLocal中,就和Thread绑定了,以后需要时,直接从ThreadLocal取就可以了。
ThreadLocal存在的问题:
1、内存泄漏:
ThreadLocal在使用时,先创建了一个ThreadLocal对象的实例,然后给这个实例指定了一个引用,这个引用就是强引用,然后在这个ThreadLocal里面Entry<k,v>这个K是指向这个ThreadLocal对象的弱引用,也就是说这个ThreadLocal对象有两个引用 一个强引用,一个弱引用,在强引用被回收后,只剩一个弱引用的时候,GC时会将这个弱引用和它指向的ThreadLocal对象回收掉,这样一来,ThreadLocalMap 中就会出现key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在,虽然存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永远不会被访问到了,所以存在着内存泄露。
但ThreadLocal内存泄漏的真正原因并不是使用了弱引用,相反弱引用的使用减少了内存泄漏的发生,因为在Entry的K为null后,如果有调用set()、get()、remove(),这几个方法调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有 remove()方法中显式调用了 expungeStaleEntry 方法。
ThreadLocal 内存泄漏的根源:由于 ThreadLocalMap 的生命周期跟Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。

ThreadLocal 导致线程不安全的场景:
因为ThreadLocalMap 中保存的其实是对象的一个引用,如果多个ThreadLocal 副本 中保存的是一个全局变量,多个引用指向同一个对象实例,那么修改一个副本的值,也会影响其他副本的值,这样会造成线程不安全的情况。
线程间的协作
指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B调用了对象 O 的 notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait()方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
等待和通知的标准范式
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
notify 和 notifyAll 应该用谁
尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
wait/notify实现生产者和消费者程序
采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,对某一个对象(枪膛)进行操作,其最大容量是20颗子弹,生产者线程是一个压入线程,它不断向枪膛中压入子弹,消费者线程是一个射出线程,它不断从枪膛中射出子弹。
代码实现:
public class TestThreadCooperate {
private static Integer gun = 0; //子弹数
private Object object = new Object(); //锁对象
class AThread extends Thread{
@Override
public void run() {
synchronized(object){
System.out.println(Thread.currentThread().getName()+"开始装弹");
while(gun < 20){
gun++;
System.out.println(Thread.currentThread().getName()+"装入子弹数量:"+gun);
}
System.out.println(Thread.currentThread().getName()+"子弹装满");
object.notifyAll(); //唤醒通知
}
}
}
class BThread extends Thread{
@Override
public void run() {
synchronized(object){
System.out.println(Thread.currentThread().getName()+"开始射击");
while(gun <= 0){
try {
object.wait(); //等待唤醒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
while(gun > 0){
System.out.println(Thread.currentThread().getName()+"剩余子弹数量:"+gun);
gun--;
}
System.out.println(Thread.currentThread().getName()+"子弹射光");
}
}
}
public static void main(String[] args) {
TestThreadCooperate testThreadCooperate = new TestThreadCooperate();
AThread aThread = testThreadCooperate.new AThread();
BThread bThread = testThreadCooperate.new BThread();
bThread.setName("射击");
bThread.start();
aThread.setName("装弹");
aThread.start();
}
}
运行结果:
装弹开始装弹
装弹装入子弹数量:1
装弹装入子弹数量:2
装弹装入子弹数量:3
装弹装入子弹数量:4
装弹装入子弹数量:5
装弹装入子弹数量:6
装弹装入子弹数量:7
装弹装入子弹数量:8
装弹装入子弹数量:9
装弹装入子弹数量:10
装弹装入子弹数量:11
装弹装入子弹数量:12
装弹装入子弹数量:13
装弹装入子弹数量:14
装弹装入子弹数量:15
装弹装入子弹数量:16
装弹装入子弹数量:17
装弹装入子弹数量:18
装弹装入子弹数量:19
装弹装入子弹数量:20
装弹子弹装满
射击开始射击
射击剩余子弹数量:20
射击剩余子弹数量:19
射击剩余子弹数量:18
射击剩余子弹数量:17
射击剩余子弹数量:16
射击剩余子弹数量:15
射击剩余子弹数量:14
射击剩余子弹数量:13
射击剩余子弹数量:12
射击剩余子弹数量:11
射击剩余子弹数量:10
射击剩余子弹数量:9
射击剩余子弹数量:8
射击剩余子弹数量:7
射击剩余子弹数量:6
射击剩余子弹数量:5
射击剩余子弹数量:4
射击剩余子弹数量:3
射击剩余子弹数量:2
射击剩余子弹数量:1
射击子弹射光
网友评论