【Java成王之路】EE初阶第三篇:(多线程3)

前言

接上一篇内容.......

产生线程不安全的原因~

5.指令重排序~(也是和编译器优化相关)

编译器会自动调整执行指令的顺序,以达到提高效率的效果

调整的前提是,保证指令的最终效果是不变的(如果当前的逻辑只是在单线程下运行.编译器判定顺序是否影响结果,就很容易.如果当前的逻辑可能在多线程下运行.编译器判定顺序是否影响结果.就可能出错)

(编译器优化,是一个非常智能的东西,哪怕程序猿代码写的很挫.但是编译器咔咔一顿优化,代码效率还是会挺高的)

如何解决线程不安全问题~ 

最普适的办法,就是通过"原子性"这样的切入点来解决问题

把一些不是原子性的操作变成原子性的操作

synchronized  英文原义"同步"

这里理解为"互斥"更合适

这个单词一定要会拼写,会念

这里我们继续来看之前用到的一个案例:两个线程分别针对count自增5w次

public class TestDome7 {
    static class Counter{
        public int count = 0;
       synchronized public void increase(){
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };


        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        t.start();
        t2.start();

        t.join();
        t2.join();

        System.out.println(counter.count);
    }
}

【Java成王之路】EE初阶第三篇:(多线程3)_第1张图片

 

如果两个线程同时并发的方式调用这个synchronized修饰的方法

此时一个线程会先执行这个方法,另外一个线程会等待,等到第一个线程方法执行完了之后,第二个线程才会继续执行

就相当于"加锁"和"解锁"

进入synchronized修饰的方法,就相当于加锁

出了synchronized修饰的方法,就相当于解锁

如果当前是已经加锁的状态,其他的线程就无法执行这里的逻辑,就只能阻塞等待

synchronized的功能本质上就是把"并发"变成"串行"

适当的牺牲一下速度,但是换来的是结果更加准确

多次打印结果:

【Java成王之路】EE初阶第三篇:(多线程3)_第2张图片

synchronized除了可以修饰方法之外,还可以修饰一个"代码块"

【Java成王之路】EE初阶第三篇:(多线程3)_第3张图片

synchronized如果是修饰代码块的时候需要显示在 () 中指定一个要加锁的对象

如果是synchronized直接修饰的非静态方法,相当于加锁的对象是this

完整代码:

public class TestDome7 {
    static class Counter{
        public int count = 0;
        public void increase(){
            synchronized (this){
                count++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };


        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        t.start();
        t2.start();

        t.join();
        t2.join();

        System.out.println(counter.count);
    }
}

 啥叫"加锁的对象"~

Java中,一个对象的内存布局大概是这样的:

【Java成王之路】EE初阶第三篇:(多线程3)_第4张图片

Java中任意的对象,都可以作为"锁对象"

synchronized刷新内存 ~

synchronized不光能够起到互斥的效果,还能够刷新内存~(解决内存可见性问题)

例如:

一个代码中进行循环++

【Java成王之路】EE初阶第三篇:(多线程3)_第5张图片

每次自增都是

LOAD

ADD

SAVE

这三个操作

但是编译器会优化这里的效率,把中间的一些LOAD,SAVE操作省略掉

变成LOAD,ADD,ADD,ADD,ADD,SAVE

加上synchronized之后,就会禁止上面的优化,保证每次进行操作的时候,都会把数据真的从内存读,也真的写回内存中.

也是让程序跑的慢一点,但是能够算的准

大家要心中有数,一旦代码中使用了synchronized,此时咱们的程序很可能和"高性能"无缘了

可重入~ 

synchronized允许一个线程针对一把锁,咔咔连续加锁两次~

【Java成王之路】EE初阶第三篇:(多线程3)_第6张图片

进入increase方法,就加了一次锁

再进入代码块,又加了一次锁

这种操作对于synchronized来说是没问题

synchronized对这里进行了特殊处理

(如果是其他语言的锁操作,这里可能就会造成死锁)

第一次加锁,加锁成功

第二次再尝试针对这个线程加锁的时候,此时对象头的锁标记已经是true.按照咱们之前的理解,线程就要阻塞等待,等待这个锁标记被改成false,然后才重新竞争这个锁......

但是仔细一想,这个锁啥时候能释放?

【Java成王之路】EE初阶第三篇:(多线程3)_第7张图片

但是synchronized为了防止程序猿犯蠢.(Java是属于对程序猿的智商评估的比较准的编程语言,而其他的语言,尤其是C++,高估了程序猿的智商)

于是就做了一个特殊处理:synchronized实现了可重入

synchronized内部记录了当前这个锁是哪个线程持有的

可重入:同一个线程连续两次针对同一个锁进行加锁操作,不会死锁

synchronized修饰普通方法的话,相当于是针对this进行加锁(这个时候如果两个线程并发的调用这个方法,此时是否会触发锁竞争就看实际的锁对象是否是同一个了)

synchronized修饰静态方法的话,相当于针对类对象进行加锁(由于类对象是单例的,两个线程并发调用该方法,一定会触发所竞争)

这就是一个类对象

反射的时候介绍过这个:

反射也是面向对象中的一个基本特性(和封装继承多态....是并列关系)

反射也叫"自省"

一个对象能够认清自己(程序运行时)

(这个对象里包含哪些属性,每个属性叫啥名字,是啥类型,public/private......

包含哪些方法,每个方法叫啥名字,参数列表是啥,public/private

这些信息来自于 .class文件(.Java被编译生成的二进制字节码)

会在JVM运行的时候加载到内存中~

就通过"类对象"来描述这个具体的.class文件的内容

类名.class就得到了这个类对象

特点:每个类的类对象都是单例的

标准库中(集合类)的线程安全的类 ~

这里的集合类,大部分是线程不安全的(不能在多线程环境下去并发修改同一个对象)

典型的:ArrayList/LinkedList/HaspMap/HashSet....都是线程不安全的

还有一些是线程安全的

Vector(在最新版本的Java16里好像已经删了,很早就已经被标记成"弃用"状态了)

这个是JDK早期内置的集合类,这里的设计不是特别科学

后续的编程中,一般不建议使用Vector,而是使用ArrayList来代替

Vector也是一个顺序表(动态数组),能自动扩容啥的.....

Vector中使用synchronized来保证了线程安全~

Vector中给这里的很多方法都加上了synchronized来修饰~

这种操作并不好!

大多数情况下并不需要在多线程中使用Vector,而如果我们加了太多的synchronized就会对单线程环境下的操作的效率造成负面影响

Stack(栈)

继承自Vector,躺枪了

和Vector类似,还有一个HashTable是一个哈希表结果.也是类似的做法,也是把很多方法都加了synchronized.结论也是不建议使用

ConcurretHashMap也是一个线程安全的哈希表

相比于HashTable来说,就设计的非常好了

StringBuffer也是

String认为也是线程安全的.但是又没有加锁

String是不可变对象~~

不可能存在两个线程并发的修改同一个String.

volatile ~

volatile 这个词也是需要大家会拼写,会读~计算机中一般理解成"可变的,容易改变的"

volatile的功能是保证内存可见性,但是不能保证原子性.

volatile使用案例:

public class TestDome8 {
    static class Counter{
        public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t = new Thread(){
            @Override
            public void run() {
                while (counter.flag == 0){
                    //假设要执行一些操作
                }
                System.out.println("循环结束");
            }
        };
        t.start();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                //让用户输入一个整数,用这个用户输入的值来替换 counter.flag的值
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数:");
                 counter.flag = scanner.nextInt();
            }
        };
        t1.start();
    }
}

为什么输入一个非0的整数后程序并没有结束

看分析:

【Java成王之路】EE初阶第三篇:(多线程3)_第8张图片

加上volatile之后

public class TestDome8 {
    static class Counter{
        //一旦给这个flag加上volatile之后,此时后续的针对flag的读写操作,就都能保证一定是操作内存了
       volatile public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t = new Thread(){
            @Override
            public void run() {
                while (counter.flag == 0){
                    //假设要执行一些操作
                }
                System.out.println("循环结束");
            }
        };
        t.start();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                //让用户输入一个整数,用这个用户输入的值来替换 counter.flag的值
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数:");
                counter.flag = scanner.nextInt();
            }
        };
        t1.start();
    }
}

volatile的用法比较单一,只能修饰一个具体的属性

此时代码中针对这个属性的读写操作就一定会涉及到内存操作了

volatile不能保证原子性

1

2

使用synchronized也能读写内存

public class TestDome8 {
    static class Counter{
        //一旦给这个flag加上volatile之后,此时后续的针对flag的读写1操作,就都能保证一定是操作内存了
         public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t = new Thread(){
            @Override
            public void run() {
                //加上synchronized 之后,此时的针对flag的操作,也会读写内存

                    while (true){
                        synchronized (counter){
                        if(counter.flag != 0){
                            break;
                        }
                    }
                }

                System.out.println("循环结束");
            }
        };
        t.start();

        Thread t1 = new Thread(){
            @Override
            public void run() {
                //让用户输入一个整数,用这个用户输入的值来替换 counter.flag的值
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数:");
                counter.flag = scanner.nextInt();
            }
        };
        t1.start();
    }
}

打印结果:

【Java成王之路】EE初阶第三篇:(多线程3)_第9张图片

volatile是和编译器优化密切相关的东西

编译器优化是一个相当复杂的事情

啥时候优化,啥时候不优化,优化的时候优化到啥程度.....

咱们把握不住

编译器优化这都是世界上最NB的程序猿经历了几十年的积累写出的程序....

有些时候,哪怕代码只是动了一小点,优化的方式可能就完全不同~~

一般来说,如果某个变量,在一个线程中读,一个线程中写,这个时候大概率需要使用volatile

volatile这里涉及到一个重要的知识点,JMM(java memory model)内存模型

【Java成王之路】EE初阶第三篇:(多线程3)_第10张图片

未完待续........

【Java成王之路】EE初阶第三篇:(多线程3)_第11张图片 

 

 

你可能感兴趣的:(java,开发语言,database,数据库,mysql)