【JavaEE】-- 多线程(初阶)2

发布于:2025-02-24 ⋅ 阅读:(17) ⋅ 点赞:(0)

3.线程的状态

3.1观察线程的所有状态

public class Demo01_Thread {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

输出结果:

NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
  1. NEW:表示创建好了一个Java线程对象,安排好了任务,但是还没有启动没有调用startO方法之前是不会创建PCB的,和PCB没有任何关系.
  2. RUNNABLE:运行+就绪的状态,在执行任务时最常见的状态之一,在系统中有对应PCB
  3. BLOCKED:等待锁的状态,阻塞中的一种
  4. WATING:没有等待时间,一直死等,直到被唤醒
  5. TIMEDWATING:指定了等待时间的阻塞状态,过时不侯
  6. TERMINATED:结束,完成状态,PCB已经销毁,但是JAVA线程对象还在

3.2线程状态和状态转移的意义

在这里插入图片描述
在这里插入图片描述
问:线程等待时需不需要设置等待时间?
答:不一定。要根据具体的业务需求来决定。

4.多线程带来的的风险-线程安全 (重点)

4.1观察线程不安全

public class Demo02_Thread {
    public static void main(String[] args) throws InterruptedException {
       Count count = new Count();
       Thread t1 = new Thread(()->{
           for (int i = 0; i < 50000; i++) {
               count.increase();
           }
       });
       Thread t2 = new Thread(()->{
           for (int i = 0; i < 50000; i++) {
               count.increase();
           }
       });

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

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

        System.out.println("count = " + count.count);
    }
}

class Count{
    public long count = 0;
    public void increase(){
        count++;
    }
}

输出结果:
在这里插入图片描述
在这里插入图片描述
程序运行的结果和预期值不一样,而且是一个错误的结果,但是我们的程序逻辑是正确的,这个现象所表现的问题称为线程安全问题

4.2 线程不安全的原因

4.2.1 线程调度是随机的

由于线程是抢占式执行的,所以执行顺序是随机的。

由于线程的执行顺序无法人为控制,抢占式执行是造成线程安全问题的主要原因,而且我们解决不了这个问题,完全是CPU自己调度的原因,和CPU核数有关。

4.2.2 修改共享数据

  1. 多个线程修改同一个变量,会出现线程安全问题。
  2. 多个线程修改不同的变量,不会出现线程安全问题。
  3. 一个线程修改一个变量,也不会出现线程安全问题。

4.2.3 原子性

原子性是在指令的层面上来说的。

我们Java程序中的一条count++语句,对应的是多条CPU指令。

  1. 从内存或者寄存器中读取count的值 LOAD
  2. 执行自增 ADD
  3. 把计算结果写回寄存器或者内存中 STORE

线程是抢占式执行的,所以可能有很多种执行顺序。
在这里插入图片描述
指令的执行过程

  1. 将count加载到t1中
    在这里插入图片描述
  2. 将count加载到t2中
    在这里插入图片描述
  3. t1中执行自增操作
    在这里插入图片描述
  4. t2中执行自增操作
    在这里插入图片描述
  5. 将t1中的count加载到主内存中
    在这里插入图片描述
  6. 将t2中的count加载到主内存中
    在这里插入图片描述

4.2.4 内存可见性

在这里插入图片描述

JMM规定,工作内存和线程之间是一一对应的。

JMM(Java Memory Model:Java内存模型)的规定

  1. 所有的线程不能直接修改内存中的共享变量
  2. 如果要修改共享变量,需要把这个变量从主内存中复制到自己的工作内存中,修改完成之后再刷回主内存。
  3. 各个线程之间不能互相通信,做到了内存级别的线程隔离。

上面的count++操作,由于是两个线程在执行,每个线程都有自己的工作内存,且相互之间不可见,最终导致了线程安全问题。

线程对共享变量的修改,,线程之间相互感知不到。

工作内存是Java层面对物理层面的关于程序所使用到的寄存器的抽象。

如果通过某种方式,让线程之间可以相互通信,称之为内存可见性。

4.2.5 指令重排序

我们写的代码在编译之后可能会与代码对应的指令顺序不同,这个过程就是指令重排序(Java层面可能会重排,CPU执行指令时也可以重排)。
指令重排序必须保证程序的运行结果是正确的,在单线程的环境中是没有任何问题的,指令重排序在逻辑上互不影响。
在这里插入图片描述

面试题
JMM的特性?
保证原子性
保证内存可见性
保证有序性(禁止指令重排序)
再去回答JMM对线程修改变量的规定
主内存–>在工作内存中修改–>刷回主内存
线程之间内存是隔离的

4.3解决之前的线程不安全问题

在这里插入图片描述

5.synchronized 关键字监视器锁monitorlock

5.1 synchronized的特性

线程A拿到了锁,别的线程如果执行被锁住的代码,必须要等到线程A释放锁如果线程A没有释放锁,那么别的线程只能阻塞等待,这个状态就是BLOCK。
先拿锁–》执行代码—》释放锁–》下一个线程再拿锁…
1. 为方法加锁

public class Demo01_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });

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

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

        System.out.println("count = " + count.count);
    }
}

class Count{
    public long count = 0;
    public synchronized void increase(){
        count++;
    }
}

输出结果:

count = 100000

2. 修饰代码块

public class Demo02_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count02 count = new Count02();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });

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

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

        System.out.println("count = " + count.count);
    }
}

class Count02 {
    public long count = 0;
    public void increase(){
        //真实业务中,在执行加锁的代码块之前有很罗的数据获取或其他的可以并行执行的逻辑
        // 1.从数据库中查询数据selectALL();
        // 2.对数据进行处理buiLd();
        // 3.其他的非修改共享变量的方法..
        
        // 当执行到修改共享变量的逻辑时,再加锁
        // 通过锁定代码块
        synchronized (this){
            count++;
        }
        //还有一些非修改共享变量的方法。
    }
}

输出结果:

count = 100000

在这里插入图片描述
synchronized只解决了原子性问题,他所修饰的代码由并行变成了串行。

5.1.1 内存可见性

在这里插入图片描述

后一个线程永远读到的是上一个线程刷回主内存的值,主内存相当于一个交换空间,线程依次写入和读取,而且是串行(顺序执行)的过程,通过这样的方式实现了内存可见性,并没有对内存可见性做技术上的处理。

synchronized实现了内存可见性。

synchronized的特性

  1. 保证了原子性(通过加锁来实现)。
  2. 保证了内存可见性(通过串行执行实现)。
  3. 不保证有序性。

总结synchronied

  1. 1.被synchronized修饰的代码会变成串行执行
  2. synchronized可以去修饰方法,也可以修饰代码块
  3. 被synchronized修饰的代码并不是一次性在CPU上执行完,而是中途可能会被CPU调度走当所有的指令执行完成之后才会释放锁
  4. 只给一个线程加锁,也会出现线程安全问题

只给一个线程加锁,也会出现线程安全问题

public class Demo501_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count501 count = new Count501();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.increase1();
            }
        });

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

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

        System.out.println("count = " + count.count);
    }
}

class Count501 {
    public long count = 0;
    public synchronized void increase(){
        count++;
    }

    public void increase1(){
        count++;
    }
}

输出结果:

count = 82228

线程获取锁:

  1. 如果只有一个线程A,那么直接可以获取锁,没有锁竞争。
  2. 线程A和线程B共同抢一把锁的时候,存在锁竞争,谁先拿到就先执行自己的逻辑,另外一个线程阻塞等待,等到持有锁的线程释放锁之后,再参与锁竞争。
  3. 线程A与线程B竞争的不是同一把锁,它们之间没有竞争关系。

5.1.2 如何判断多个线程竞争的是同一把锁

如何描述一把锁?锁与线程之间如何关联?
锁对象,本身就是一个简单对象,任何对象都可以作为锁对象。
a. 实例对象:new出来的对象。
b. 类对象
锁对象中记录了获取到锁的线程信息(线程地址)。

在这里插入图片描述

5.2 synchronized的特性

5.2.1 互斥

一个线程获取了锁之后,其他线程必须要阻塞等待,只有当持有的线程把锁释放之后,所有线程再去竞争锁。
在这里插入图片描述

5.2.2 可重入

在这里插入图片描述
在方法的调用链路中,存在多个被synchronized修饰的方法单个线程这种情况下存不存在锁竞争?
对于同一个锁对象和同一个线程,如果可以重复加锁,称之为不互斥,称之为可重入。
对于同一个锁对象和同一个线程,如果不可以重复加锁,称之为互斥就会形成死锁。
已经获取锁对象的线程,如果再进行多次的加锁操作,不会产生互斥现象。

5.2.3 可见性

从结果上看是达到了内存可见性的目的,但是是通过原子性来实现的。

5.3 synchronized使用示例

  1. 执行不同对象的synchronized方法 ,修改全局变量。
public class Demo502_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count502 count = new Count502();
        Count502 count1 = new Count502();

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

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count1.increase();
            }
        });

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

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

        System.out.println("count = " + count.count);
    }
}

class Count502 {
    public static long count = 0;
    public synchronized void increase(){
       count++;
    }
}

输出结果:

count = 85541
  1. synchronized修饰方法时,锁对象就是当前对象。
  2. 这两个对象不是同一个实例,也意味着两个线程的锁对象 不同,不存在锁竞争关系,所以存在线程安全问题,输出结果错误。
  1. 使用单独的锁对象
public class Demo503_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count503 count = new Count503();

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

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

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

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

        System.out.println("count = " + count.count);
    }
}

class Count503 {
    public static long count = 0;

    //单独定义一个对象作为锁对象使用
    Object locker = new Object();
    public synchronized void increase(){
        //只锁定代码块
       synchronized (locker){
           count++;
       }
    }
}

输出结果:

count = 100000 

线程在锁竞争的时候通过locker这个对象记录线程信息。
这两个线程的锁对象都是locker,存在锁竞争关系。

  1. 在多个实例中使用单独的锁对象
public class Demo504_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count504 count = new Count504();
        Count504 count1 = new Count504();

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

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count1.increase();
            }
        });

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

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

        System.out.println("count = " + count.count);
    }
}

class Count504 {
    public static long count = 0;

    //单独定义一个对象作为锁对象使用
    Object locker = new Object();
    public synchronized void increase(){
        //只锁定代码块
       synchronized (locker){
           count++;
       }
    }
}

输出结果:

count = 98497

每个count实例中都有一个locker,两个实例的锁对象是不同的,不存在锁竞争关系。

  1. 单个实例中,创建两个方法,使用同一个锁对象
public class Demo505_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count505 count = new Count505();

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

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

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

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

        System.out.println("count = " + count.count);
    }
}

class Count505 {
    public static long count = 0;

    //单独定义一个对象作为锁对象使用
    Object locker = new Object();

    public synchronized void increase(){
        //只锁定代码块
       synchronized (locker){
           count++;
       }
    }
    
    public synchronized void increase1(){
        //只锁定代码块
       synchronized (locker){
           count++;
       }
    }
}

输出结果:

count = 100000

locker是同一个对象,都是count中的成员变量,会产生锁竞争。

  1. 使用静态全局对象作为锁对象
public class Demo506_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count506 count = new Count506();
        Count506 count1 = new Count506();

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

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count1.increase();
            }
        });

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

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

        System.out.println("count = " + count.count);
    }
}

class Count506 {
    public static long count = 0;

    //单独定义一个对象作为锁对象使用
    // 全局变量,属于类对象
    static Object locker = new Object();

    public synchronized void increase(){
        //只锁定代码块
       synchronized (locker){
           count++;
       }
    }
}

输出结果:

count = 100000

静态全局变量,属于类对象,全局只有一个,在所有的实例对象之间共享,产生锁竞争。

  1. 用类对象作为锁对象
public class Demo507_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count507 count = new Count507();

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

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

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

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

        System.out.println("count = " + count.count);
    }
}

class Count507 {
    public static long count = 0;

    public synchronized void increase(){
        synchronized (Count507.class){
            count++;
        }
    }
}

输出结果:

count = 100000

类对象是全局唯一的,产生锁竞争。

  1. 使用String.class作为锁对象
public class Demo507_Thread {
    public static void main(String[] args) throws InterruptedException {
        Count507 count = new Count507();

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

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

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

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

        System.out.println("count = " + count.count);
    }
}

class Count507 {
    public static long count = 0;

    public synchronized void increase(){
        synchronized (String.class){
            count++;
        }
    }
}

输出结果:

count = 100000

任何一个对象都可以作为锁对象,只要多个线程访问的锁对象是同一个,那么他们就存在竞争关系,否则没有竞争关系。
String.class是一个类对象,且全局唯一。

5.4 Java标准库中的线程安全类

Java 标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,⼜没有任何加锁措施.

ArrayList、 LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder

但是还有⼀些是线程安全的.使⽤了⼀些锁机制来控制.

Vector(不推荐使⽤)、HashTable(不推荐使⽤)、ConcurrentHashMap、StringBuffer

还有的虽然没有加锁,但是不涉及"修改",仍然是线程安全的

String