美文网首页
线程的共享和协作

线程的共享和协作

作者: 心清目明 | 来源:发表于2020-06-08 10:12 被阅读0次

线程共享

每个线程运行时,都会有各自的栈空间,如果线程仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。

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的对象,只是保存的是该对象实例的弱引用(为了减少内存泄漏)。如下图:

shuu

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

射击子弹射光

相关文章

网友评论

      本文标题:线程的共享和协作

      本文链接:https://www.haomeiwen.com/subject/pvyvzhtx.html