JavaEE初阶-线程2

文章目录

  • 一、多线程安全问题
    • [1.1 线程安全问题的原因](#1.1 线程安全问题的原因)
    • [1.2 如何解决线程安全问题](#1.2 如何解决线程安全问题)
  • 二、加锁
    • [2.1 synchronized](#2.1 synchronized)
    • [2.2 synchronized的几种使用方式](#2.2 synchronized的几种使用方式)
    • [2.3 synchronized的可重入性](#2.3 synchronized的可重入性)
  • 三、死锁
    • [3.1 死锁的必要条件](#3.1 死锁的必要条件)

一、多线程安全问题

代码示例如下:

java 复制代码
public class Demo20 {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {

            for (int i = 0; i < 50000; i++) {
                count++;

            }

        });

        Thread t2= new Thread(() -> {

            for (int i = 0; i < 50000; i++) {
                count++;

            }

        });

        //串行执行
//        t1.start();
//        t1.join();
//        t2.start();
//        t2.join();

        t1.start();

        t2.start();

        t1.join();
        t2.join();
        System.out.println("count:"+count);

    }
}

这段代码每次执行的结果都不一样,都没有达到预期的结果即100000。主要的原因在于:

(1)count++这个操作在cpu指令的角度上看其实是三个指令

  • load:把内存中的数据加载到寄存器。
  • add:把寄存器中的值+1。
  • 把寄存器中的值写回内存。

(2)两个线程并发进行count++,多线程的执行是随即调度,抢占式的执行模式。

综合以上两点,实际并发执行时,两个线程的指令执行的相对顺序就存在多种可能,不同执行顺序下得到的结果就会有差异,例如:

光这一种情况,得到的结果就不可能时100000。

1.1 线程安全问题的原因

(1)线程在系统中是随机调度的,抢占式执行。

这个无法改变,是内核设计者的考虑。

(2)多个线程修改同一个变量。

一个线程修改一个变量=>没事

多个线程读取一个变量=>没事

多个线程修改不同的变量=>没事

(3)线程针对变量的修改操作,不是"原子"的。

(4)内存可见性。

(5)指令重排。

1.2 如何解决线程安全问题

从原因入手:

(1)原因1:无法干预。

(2)原因2:是一个切入点,但是在java中这种做法不是很普适,只针对一些特定场景是可以做到的。

(3)原因3:这是解决线程安全问题最普适的方案。可以通过一些操作,将非原子的操作打包成一个原子的操作。(加锁)

二、加锁

加锁就是针对原因3解决线程安全问题的操作。

锁的几个特点:

(1)锁涉及的几个操作:加锁和解锁。

(2)锁的主要特性:互斥。

一个线程获取到一个锁之后,如果其它线程也想要获取该锁,会进行阻塞等待。

(3)代码中可以创建多个锁。

只有多个线程竞争同一把锁才会互斥,针对不同的锁则不会。

2.1 synchronized

synchronized后面带上()里面写的就是"锁对象"。
注意:锁对象的用途有且仅有一个,就是用来区分两个线程是否对同一个对象加锁。如果是就会互斥,引起阻塞等待。如果不是,就不会出现锁竞争,也不会阻塞等待

synchronized后面还跟着{},进了代码块就是对()内的锁对象进行了加锁,出了代码块就是对()的锁对象进行了解锁。

用加锁解决线程安全问题代码示例如下:

java 复制代码
public class Demo23 {
    public static int count=0;

    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (object) {
                    count++;
                }
            }
        });

        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (object) {
                    count++;
                }
            }
        });

        t1.start();;
        t2.start();;

        t1.join();
        t2.join();


        System.out.println(count);
    }
}

每次执行count++会因为锁竞争,从而强制变为串行执行 ,但是执行for循环的条件以及i++都是并发执行 的。和join等待操作相比,join操作时是等一个线程执行完了 再让join返回,第二个线程才能继续执行。
上述代码t1释放锁之后,下一次拿到锁的是t1还是t2仍然是概率性问题。

2.2 synchronized的几种使用方式

在示例方法内加锁及给类方法加锁:

java 复制代码
class Counter {
    public int count = 0;

    synchronized public void add() {
//        synchronized (this) {
//
//        }
        count++;
    }

    synchronized public static void func() {
//        synchronized (Counter.class) {
//
//        }
    }

    public int get() {
        return count;
    }
}


public class Demo24 {


    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Counter counter1 = new Counter();
        Thread t1 = new Thread(() -> {
//            synchronized (counter) {
//                counter.add();
//            }
            for (int i = 0; i < 5000; i++) {
                //counter.func();
                counter.add();
            }

        });

        Thread t2 = new Thread(() -> {
//           synchronized (counter) {
//               counter.add();
//           }

            //counter.add();
            for (int i = 0; i < 5000; i++) {
                //counter1.func();
                counter.add();
            }
        });


        t1.start();
        t2.start();
        t1.join();
        t2.join();

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

}

2.3 synchronized的可重入性

java 复制代码
class Counter1 {
    private int count = 0;

    synchronized public void add() {
        count++;
    }

    public int get() {
        return count;
    }

}

public class Demo25 {
    public static void main(String[] args) throws InterruptedException {
        Counter1 counter=new Counter1();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
               //相当于
//                synchronized (counter) {
//                    synchronized (counter) {
//                        count++;
//                    }
//                }
                //按理说这样写应该会阻塞,因为外括号已经在counter上加了锁,内括号再加就会堵塞
                //内等待外执行完但是外又在等内执行完,于是进入死锁
                //但是java中不会这样,因为如果在java中内外是一个锁对象则直接进入
                //理论上上这是线程死锁的情况一
                synchronized (counter) {
                    counter.add();
                }

            }

        });

        Thread t2=new Thread(()->{

            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });


        t1.start();
        t2.start();
        t1.join();
        t2.join();

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

}

上述代码中的t1按理应该进入死锁,但是因为java中的synchronized具有可重入性所以避免了死锁,但是在其它语言中都是会死锁的。

三、死锁

死锁的三种典型场景:

  • 锁的不可重入性,一个线程获取了一个锁,如果再对这个锁加锁就会产生死锁。不过引入可重入锁就可以解决,并且java中的用来加锁的synchronized具有可重入性所以无需考虑这种情况。
  • 两个线程两把锁。例如线程1加锁a,线程2加锁b,线程a又请求锁b,线程2又请求锁a此时就会死锁。
  • 多个线程多把锁。例子就是经典的哲学家就餐问题。

3.1 死锁的必要条件

(1)锁具有互斥性,一个线程获取锁a,如果另一个线程也想获取锁a就得阻塞等待。

(2)锁具有不可剥夺性:一个线程获取锁a,除非它主动释放,其它线程无法夺取线程上的锁a。

(3)请求和保持:一个线程获取锁a,在不释放该锁的前提下,去获取其它的锁。

(4)循环等待:多个线程获取多个锁的过程中出现循环等待。

这四个必要条件缺一不可,任何死锁的情况都必须具备以上四点否则无法构成死锁。

如果想避免死锁,可以打破上述的四个必要条件。条件一二如果是自己实现的锁那么可以打破,但是在java的synchronized中这两条是无法打破的,但是可以从后面两条来进行打破。

打破条件三是要从代码结构上进行改变,因为条件三会造成锁的嵌套,只要让自己建立的锁不嵌套即可,当然有的时候会不得不去嵌套锁。

打破条件四只需要规定好加锁的顺序即可。

代码示例如下:

java 复制代码
public class Demo26 {

    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("t1 获取了两把锁");
                }
            }

        });

        Thread t2 = new Thread(() -> {
            synchronized (locker1) {

                synchronized (locker2) {
                    System.out.println("t2 获取了两把锁");
                }
            }
        });
        //以上代码会发生线程死锁的情况二
        //可以通过修改代码结构来避免死锁
        //避免循环等待:约定加锁的顺序
        //Thread t1 = new Thread(() -> {
        //            synchronized (locker1) {
        //                try {
        //                    Thread.sleep(1000);
        //                } catch (InterruptedException e) {
        //                    throw new RuntimeException(e);
        //                }
        //                synchronized (locker2) {
        //                    System.out.println("t1 获取了两把锁");
        //                }
        //            }
        //
        //        });
        //
        //        Thread t2 = new Thread(() -> {
        //            synchronized (locker1) {
        //
        //                synchronized (locker2) {
        //                    System.out.println("t2 获取了两把锁");
        //                }
        //            }
        //        });

        //避免请求和保持:
        //Thread t1 = new Thread(() -> {
        //            synchronized (locker1) {
        //                try {
        //                    Thread.sleep(1000);
        //                } catch (InterruptedException e) {
        //                    throw new RuntimeException(e);
        //                }
        //
        //            }
    //               synchronized (locker2) {
//                         System.out.println("t1 获取了两把锁");
        //            }
        //        });
        //
        //        Thread t2 = new Thread(() -> {
        //            synchronized (locker2) {
        //
        //
        //            }
        //        synchronized (locker1) {
        //                 System.out.println("t2 获取了两把锁");
        //         }
        //        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();


    }
}
相关推荐
程序媛小果7 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林12 分钟前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨27 分钟前
El表达式和JSTL
java·el
duration~1 小时前
Maven随笔
java·maven
zmgst1 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong2 小时前
Java反射
java·开发语言·反射
九圣残炎3 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode