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();


    }
}
相关推荐
武子康6 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘1 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意1 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
东阳马生架构2 小时前
JVM实战—1.Java代码的运行原理
jvm
FF在路上2 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.3 小时前
Mybatis-Plus
java·开发语言
不良人天码星3 小时前
lombok插件不生效
java·开发语言·intellij-idea
守护者1703 小时前
JAVA学习-练习试用Java实现“使用Arrays.toString方法将数组转换为字符串并打印出来”
java·学习