java高并发程序设计(二)线程状态,sychronized,wait,notify,AtomicInteger,CountDownLatch

线程同步之sychronized细究

1.1 关于sychronized的小例子
多线程环境下,如果采用有锁编程可以考虑Sychronized和ReentrantLock,首先介绍一下sychronized

上面的代码中,有个类T,成员变量count和new一个对象o专门来当锁对象,sychronized锁住的是这个对象o,而不是代码块

在方法m中,线程要执行被sychronized包围的代码首先要去堆内存中申请o这个对象的锁,因为o这个对象的锁信息记录在堆内存中o对象中,当第一个线程申请到这把锁以后,就可以执行代码块,第二个线程在第一个线程执行完毕之前获取不到这把锁,因此synchronized这叫互斥锁

注意:此处是堆内存中的o对象而不是栈内存中对象的引用,因此当栈内存中的引用指向新的对内存地址时候,锁对象就会变化

然后我们发现,上面的代码中这把锁使我们自己new出来的,但是每次new一个对象仅仅作用是为了当锁对象,这显然是很麻烦的,那么有没有简便的方法呢?答案肯定是有的

1.2 关于如何优化1.1小例子
我们可以直接使用this来指代本类对象来当锁,当有人要调用m方法的时候,必须得先new一个这个对象出来,然后把自身这个对象锁定,每次执行m方法,都先要锁定自身

关于同步sychronized的理解可以参照下图,张三李四王五去上wc,一次只能进去一个人,进去的时候会锁门其他人就进不去,只有上完了把门打开,走出来,下一个人才能获得锁,进去

1.3 对1.2小例子进行写法上的优化
下面代码等效于1.2代码

1.4 将synchronized用在静态方法上
如果synchronized用在静态方法上,则代表锁的对象是本来的Class对象,因为静态的时候是没有this的,因为静态的方法静态的属性不需要new出对象就可以访问的,所以这里是不可以的

public class Test1 {

    private static int count = 1;

    // 把synchronized用在静态方法上,等效于synchronized(Test1.class)
    public synchronized static void m() {
        count++;
        System.out.println(Thread.currentThread().getId());
    }

    // 上面的写法等效于下面的
    public static void mm() {
        synchronized(Test1.class) { // 这里不能锁this对象,因为静态方法不需要创建对象就可以调用
            count++;
            System.out.println(Thread.currentThread().getId());
        }
    }
}

1.5 多线程条件下为何要使用synchronized?
这里写图片描述
上面的代码中,定义一个实现了runnable接口的任务类,在main方法中创建他的对象t,然后这个t指向堆内存中T的对象,T中有一个count,count为10,然后创建5个线程并传入这个t对象,进行count–操作,并打印count的值,需要注意:
不是每个线程里都new了一个t,而是一共只new了一个对象,好多线程共同访问这个对象

启动5个线程,共同访问的是同一个t,共同拥有的都是t的对象,每个线程都是访问t对象的中的run方法,而run方法中访问的coun自然都是同一个,执行结果如下:

按理说每个线程进去,都减1,然后打印,为什么会有3个重复的值呢?
我们可以这么理解:

第一个线程进来变成9,还没来得及打印之前,第二个线程进来这个数减到8,第三个线程进来这个数减到7然后打印出7,这样就造成了上述的问题,那么怎么解决呢?
解决的方法:就是在run方法上加上synchronized,被sychronized包装的代码块是原子操作,是不可再分的,添加后,打印如下

线程1,现在的count值是9
线程4,现在的count值是8
线程5,现在的count值是7
线程3,现在的count值是6
线程2,现在的count值是5

1.6 同步方法和非同步方法是否可以同时使用?

扫描二维码关注公众号,回复: 1481807 查看本文章
public class Test3 {

    synchronized void m1() {

        System.out.println(Thread.currentThread().getName()+" m1 start");

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+" m1 end");
    }

    void m2() {

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+" m2 ***");
    }

    public static void main(String[] args) {
        Test3 t = new Test3();
        new Thread(t::m1,"线程1").start();
        new Thread(t::m2,"线程2").start();
    }
}

第一个线程执行党的是t对象的m1方法,第二个线程执行的t对象的m2方法
m1睡10秒钟,m2睡5秒钟,如果m2打印了说明m2是可以运行的,结果如下

线程1 m1 start
线程2 m2 ***
线程1 m1 end

可以这么理解:m1是上厕所的人,他要上需要申请这把锁,m2是外面打扫卫生的大妈她不需要
执行m1的时候是锁定整个对象,但是执行m2的时候,他不需要看那把锁,不管你锁不锁定,都直接执行
1.7 银行取钱,认为读的方法不用加锁,写的方法采用加锁?

/**
 * 模拟银行取钱,对写加锁,对读不加锁
 */
public class Test4 {

    String name;    // 户主
    double money;   // 账户剩余的钱

    public synchronized void set(String name,double money) {
        this.name = name;
        try {
            Thread.sleep(2000); // 扩大赋值name和money之间的时间差,以产生读取0结果
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.money = money;
    }

    public double get(String name) {
        return this.money;
    }

    public static void main(String[] args) {
        Test4 t = new Test4();
        //创建一个线程调用set方法
        new Thread(()->t.set("周杰伦", 100)).start();;
        try {
            Thread.sleep(1000); // 让主线程睡1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.get("周杰伦"));
        try {
            Thread.sleep(2000); // 让主线程睡1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.get("周杰伦"));
    }

}

上面代码的执行结果:

0.0
100.0

原因:
尽管在写的方法上加上了synchronized锁这个对象,锁定这个对象过程中,他仍然有可能被那些非锁定【非同步】的方法如getBalance()去访问的

模拟赋值name和money之间的时间差,睡2秒的意思就是模拟在这两个操作之间可能的其他线程执行非同步的比如getBalance()这种读业务代码,导致脏读现象
解决:
很简单,在读的方法上也加锁,加的是同一把锁,因此在设置完成之前其他线程是进不去的

100.0
100.0

1.8 一个线程已经拥有该对象的锁,再次申请能否重新获得该对象的锁(sychronized锁能否重入)?

public class Test5 {

    synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();   // 
    }

    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);  // 保证先从
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }
}

给堆内存中的T对象加上了右边这把锁
调用m2,当前线程就会再去申请当前持有的这把锁,没问题,是可以得到的,即synchronized获得的锁是可重入的,可重入是获得以后还可以再获得一遍只不过那个锁上加了一个标记2
上面是一个线程做的事情:同一线程同一把锁

1.9 synchroized中遇到异常

/**
 * 程序执行过程中,如果抛出异常,默认情况锁会被释放
 */
public class Test6 {

    int count=0;

    synchronized void m() {
        System.out.println("m start");
        while(true) {
            count++;
            System.out.println(Thread.currentThread().getName()+" count="+count);

            try {
                TimeUnit.SECONDS.sleep(1);  // 隔一秒打印一次,防止速度过快
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if(count==5) {  // 满足条件抛出异常,如果锁不被释放,永远不会执行线程2
                System.out.println(1/0);
            }
        }
    }

    public static void main(String[] args) {
        Test6 t = new Test6();
        new Thread(t::m,"线程1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(t::m,"线程2").start();
    }

}

打印结果如下:

m start
线程1 count=1
线程1 count=2
线程1 count=3
线程1 count=4
线程1 count=5
m start
线程2 count=6
Exception in thread "线程1" java.lang.ArithmeticException: / by zero
    at com.mrfy.Test6.m(Test6.java:25)
    at java.lang.Thread.run(Unknown Source)
线程2 count=7
线程2 count=8
线程2 count=9

可以看到线程2执行了,如果不想释放那把锁,就try…catch
引用1.7的例子,如果在设置name和balance中间出了个异常,另外的线程进去了,他就读到了改了一半的name,以及没有改的balance,就会很容易产生问题,因此多线程的时候要把这个异常好好处理
1.10 volatile使用保证内存可见性

public class Test7 {

    boolean flag = true;

    void m() {
        System.out.println("m start");
        while(flag) {

        }
        System.out.println("m start");
    }

    public static void main(String[] args) {
        Test7 t = new Test7();
        new Thread(t::m,"线程2").start();
        try {
            TimeUnit.SECONDS.sleep(1);  // 主线程沉睡一秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.flag = false;
    }

}

上面代码按理说,在堆内存中创建了T对象,让栈内存中的t引用指向它,然后开启一个线程调用m方法,然后主线程在沉睡一秒之后设置标志位flag为false以使得m方法中的循环退出,然后事实是m方法并没有退出,在打印了m start后一直处于等待状态,这是为什么呢?

原因:
有主内存,每个线程同时持有自己的工作内存,主内存中每个线程需要用到的共享变量都会复制一份到线程的工作内存中,这样拷贝了几份,当某个线程对共享变量做修改以后,其他线程未必知道这个修改

在上面的代码中,flag是存在于堆内存的T对象中,当线程2开始运行的时候,会把flag从堆内存中读取到线程2的工作内存中,在运行过程中直接使用这个拷贝,并不会每次都去读取堆内存,这样,当主线程修改flag的值后,线程2不知道这个更改,所以不会停止,除非这个修改提交到了主内存,同时其他线程再去重新读取主内存中的值,这样才能确保其他线程获得的结果是最新正确的,当然cpu的工作机制决定了如下的操作:

当cpu忙碌的处于业务执行时间的时候,它很大几率上只会从缓存中读取变量的值,当cpu空闲执行如system.out这样的代码的时候,cpu可能抽空回去主内存中刷新数据。那么我们如何才能确保一个线程做出的修改能即时被其他线程看到呢?这里我们用到了volatile,只需要在下面的变量声明中加入volatile

volatile boolean flag = true;

然后我们就会看到线程2看到了主线程对于flag变量的修改,程序成功结束

m start
m end

可以这样理解:加入volatile以后就是即时将数据修改刷新到主内存中同时通知其他线程你缓冲区中的数据过期了,请你从主内存中重新读取

1.11 volatile无法保证操作的原子性

public class Test8 {

    volatile int count = 0;

    void m() {
        for(int i=0;i<10000;i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        Test8 t = new Test8();
        List<Thread> threads = new ArrayList<Thread>();
        for(int i=0;i<10;i++) {
            threads.add(new Thread(t::m,"线程"+i));
        }
        threads.forEach(x->x.start());
        threads.forEach(x->{    // 使得主线程等待所有new线程执行完毕
            try {
                x.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }

}

按理说,每个线程加1w次,10个线程最后加下来的结果应该是10w,可是最后执行结果:32632
远远小于100000,这是为什么呢?不是加了volatile了嘛
原因:volatile不能保证操作的原子性,比如第一个线程加,加到100了,还没往上加的时候,另一个线程看到了这个100,然后拿到自己缓冲区,然后做+1操作变成101写到主内存,然后第一个线程也执行+1操作,写回主内存,写回的时候可不管主内存中现在的count值是多少,因为它之前读到的是100,这样就把101覆盖了一遍,所以虽然+了2,但是最后count值只+了1

保证可见性只是保证读到的数是最新的,比如第三个线程过来它肯定读到的是101,但是它加到102,是不会管你现在的count是102的

上面问题解决方式1:有锁的话 sychronized既保证可见性,又保证原子性

    int count = 0;
    synchronized void m() {
        for(int i=0;i<10000;i++) {
            count++;
        }
    }

上面问题解决方式2:使用AtomicInteger类
如果只是设计++/–简单的数据运算,用AtomicInteger代替int,用count.incrementAndGet来代替count++,因为countInrementAndGet是具备原子性的,一个线程执行这个的时候,另一个线程无法去打断他

    AtomicInteger count = new AtomicInteger(0);

    void m() {
        for(int i=0;i<10000;i++) {
            count.incrementAndGet();
        }
    }

执行结果:100000

如果先if,再执行atomic操作,这两句话构成的部分就不具备原子性操作了
虽然count.get()和count.incrementAndGet()都有原子性,但是在他们两个原子性操作中间,另外的线程还是有可能插入进去

    void m() {
        for(int i=0;i<10000;i++) {
            // if count.get()<1000
            count.incrementAndGet();    // count++
        }
    }

首先一个线程读取到count值为999,他还没有加的时候
第二个线程进来也读取到999,然后他加了1,count变成1000
然后第一个线程之前已经判断完了,给把count1000再加1变成1001
即本来1000就不会进入方法,但是现在就是进入了还变成了1001

1.12 同步代码块中的语句数量影不影响执行效率?
肯定影响,锁定代码多的是粗粒度锁,锁定代码少的是细粒度锁

1.13 锁定对象的属性发生改变,影响锁的使用吗?锁定对象的引用发生改变呢?

public class Test10 {

    Object o = new Object();    // 创建锁对象

    void testLock() {
        synchronized(o) {
            while(true) {
            try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"在打印");
            }
        }
    }

    public static void main(String[] args) {
        Test10 t = new Test10();
        // 创建第一个线程
        new Thread(t::testLock,"t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.o = new Object(); // 改变锁的引用,致使锁发生改变,锁失效     如果没有这句,线程2将永远得不到执行
        //创建第二个线程
        new Thread(t::testLock,"t2").start();
    }

}

执行结果:

t1在打印
t1在打印
t1在打印
t2在打印
t1在打印
t2在打印
t1在打印
t2在打印
t1在打印

Object o是当做锁来用的,sychronized锁定了这个对象
锁的堆内存中的对象,不是占内存中的引用,锁的信息是记录在堆内存中的
当执行 t.o = new Object()的时候,锁的对象发生改变了,我就不用锁原来的对象,而新对象还没有锁,所以我t2就可以执行了

1.14 实现一个容器,实现两个方法,add和size,写两个线程,线程1添加10个元素到容器中,线程2监控元素的个数,个数到5时候线程2给出提示并退出

public class Test11 {

    List list = new ArrayList();

    void add(Object o) {
        list.add(o);
    }

    int size() {
        return list.size();
    }

    public static void main(String[] args) {
        Test11 t = new Test11();
        new Thread(() ->{
            for(int i=0;i<10;i++) {
                t.add(new Object());
                System.out.println("add"+i);
                try {
                    TimeUnit.SECONDS.sleep(1);  // 每隔一秒加一次
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();

        new Thread(() ->{
            while(true) {
                if(t.size()==5) {
                    break;
                }
            }
            System.out.println("容器中元素个数5");
        },"t2").start();
    }
}

按照上面代码书写,可以发现运行结果:

add0
add1
add2
add3
add4
add5
add6
add7
add8
add9

这是为什么呢?按理说应该到5,线程2就打印信息并退出啊?
原因:不可见性,线程1对工作内存的操作未及时刷新到主内存,线程2不能够及时看到这个改变,解决方式1:
添加volatile该list,保证线程2可以看到list的在线程1操作下的及时改变

volatile List list = new ArrayList();

打印结果:

add0
add1
add2
add3
add4
容器中元素个数5
add5

可以发现,我们实现了我们的预期目标,但是这么写不够有特点,我们能不能想到别的方法呢?答案是肯定的,这里就出来了方法2
在方法2说明以前,可以有这样一个例子:在一个排好序的数组中,计算数组的所有数的总和

我们可以考虑这个数组足够大, 我们将数组分成n块,开启多个线程每个线程负责一块,然后把每个线程的运算结果汇总这样效率就比较高

回到解决方式1,我们可以发现有两个缺点:
第一个:线程2在if和里面的break的时候可能因为没有线程同步,导致if完了,还没break的时候,线程1已经将结果加到6,所以这是不合理的
第二个:线程二的判断代码一直在whlle中,所以t2的死循环是很耗费cpu资源的

结合上面两点,我们引入了解决方式2,使用wait和notify搭配,使得等到容器中元素为5的时候再去通知线程2,注意:使用wait和notify必须锁定对象,不锁定是不能调用这个对象的wait和notify方法的!!!

至于wait和notify:

首先,有一个对象,有两个线程需要去访问
在第一个线程访问的过程中,如果条件还没满足,就让这个线程暂停等者
还没到5的时候,你先给我等着
过程:锁定这个对象,然后在这个对象上调用这个对象的wait方法,当前线程进入等待状态,同时释放锁,别的线程可以进来
只有在调用被锁定对象的notify方法/notify All,才会叫醒等待在这个对象上的某个线程
通过其他线程调用notify方法,因为等待的线程在停滞

wait释放锁,notify不会释放锁,使用wait和notify的话必须要先保证t2先执行,也就是首先让t2监听才可以,另外java中线程的优先级是很微弱的

public class Test12 {

    volatile List list = new ArrayList();

    void add(Object o) {
        list.add(o);
    }

    int size() {
        return list.size();
    }

    public static void main(String[] args) {
        Test11 t = new Test11();
        final Object o = new Object();  // 创建的锁对象

        new Thread(() ->{
            synchronized (o) {
                if(t.size()<5) {
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("线程2退出");
        },"t2").start();

        // 等待1秒执行t1线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        new Thread(() -> {
            synchronized(o) {
                for(int i=0;i<10;i++) {
                    t.add(new Object());
                    System.out.println("add "+i);

                    // 唤醒在对象o上等待的线程,如多个,随机唤醒
                    if(t.size()==5) {
                        o.notify();
                    }

                    try {
                        TimeUnit.SECONDS.sleep(1);  // 每隔一秒加一次
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"t1").start();

    }

}

上面的代码执行结果依然是线程1不断

t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
add 5

这是为什么呢?因为线程2刚开始进去,容器中元素个数为2因此线程2进入等待,释放锁,然后线程1执行,虽然唤醒了线程2,但是这时候锁还是在线程1,所以线程1执行完毕之前,线程2永远得不到执行

解决如下:
修改线程1和线程2代码如下

new Thread(() ->{
            synchronized (o) {
                System.out.println("t2启动");
                if(t.size()<5) {
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程2退出");
                o.notify(); // 唤醒等待在锁o上的线程1
            }
        },"t2").start();
new Thread(() -> {
            synchronized(o) {
                System.out.println("t1启动");
                for(int i=0;i<10;i++) {
                    t.add(new Object());
                    System.out.println("add "+i);

                    // 唤醒在对象o上等待的线程,如多个,随机唤醒
                    if(t.size()==5) {
                        o.notify();
                        try {
                            o.wait();   // 让线程1进入等待状态,释放锁给与线程2执行机会
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }


                    try {
                        TimeUnit.SECONDS.sleep(1);  // 每隔一秒加一次
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"t1").start();

执行结果

t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
线程2退出
add 5
add 6
add 7

可以看到已经成功解决这个问题,不过我们可以看到方式2的缺点:比较繁杂,人为控制,因此有没有更加简便且有效地方法呢?
方式3:使用countDownLatch门栓:

public class Test13 {

volatile List list = new ArrayList();

    void add(Object o) {
        list.add(o);
    }

    int size() {
        return list.size();
    }

    public static void main(String[] args) {
        Test11 t = new Test11();

        CountDownLatch latch = new CountDownLatch(1);   // 创建一个门栓

        new Thread(() ->{
            System.out.println("t2启动");
            if(t.size()<5) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程2退出");
        },"t2").start();

        // 等待1秒执行t1线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        new Thread(() -> {
            System.out.println("t1启动");
            for(int i=0;i<10;i++) {
                t.add(new Object());
                System.out.println("add "+i);

                // 唤醒在对象o上等待的线程,如多个,随机唤醒
                if(t.size()==5) {
                    latch.countDown();// 减去一个门栓,如果见减到0了,门栓就开了
                }

                try {
                    TimeUnit.SECONDS.sleep(1);  // 每隔一秒加一次
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();

    }

}

上面代码执行结果

t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
线程2退出
add 5
add 6

可以看到完全正确
过程:首先指定一个门栓:countDownLatch,是countDown门栓。倒数门栓,当latch里面的数字从1变成0的时候门就开了
首先启动t2线程,当list的大小不是5的时候就让门栓latch等待,门栓等待意思就是整个程序在这里等待,跟wait作用一样,但是门栓等待是不需要锁定任何对象的
调用门栓的countDown方法,后面的数字就减去1,变成0

猜你喜欢

转载自blog.csdn.net/bsfz_2018/article/details/80050838