[Java EE] 多线程 -- 初阶(2)

4 .线程的状态

java 复制代码
public class demo13 {
    public static void main(String[] args) {
        for (Thread.State s:Thread.State.values()) {
            System.out.println(s);
        }
    }
}
  • NEW : 安排了工作 , 还没开始行动 ; new 了 thread 对象 , 还没 start() , 不具备运行条件
  • RUNNABLE : 可工作的 , 又分为 ① 正在运行 , 线程在 cpu 上运行 和 ② 就绪状态 , 线程随时可以去 cpu 上执行 ; 调用 start()线程就进入了就绪状态
  • BLOCKED : 线程因竞争对象锁失败(进入 synchronized**代码块时被其他线程持有锁) , 暂时停止运行 , 进入阻塞状态 ; 当锁被释放后 , 线程会重新进入就绪状态( RUNNABLE )等待调度
  • WAITING : 线程通过调用无超时的等待方法 (Object.wait() , Thread.join() , LockSupport.park() ) 进入此状态 ; 线程不会主动唤醒 , 需要等待其他线程显示唤醒 ( 如 Object.notify() ) , 否则一直等待
  • TIMED_WAITING : 线程通过调用带有超时的等待方法(Object.wait(long) , Thread.sleep(long) , Thread.join(long) )进入此状态 ; 与 WAITING的区别是 : TIMED_WAITING 超时会自动唤醒 , 重新进入就绪状态
  • TERMINATED : 执行完毕或者 ( run()方法结束 ) 或因异常退出 , 进入终止状态 ; 此时线程生命周期结束 , 无法再被启动 ( 再次调用 start() 会抛异常)

状态转换关系:

  • 新建(New)→ 就绪(Runnable):调用start()方法
  • 就绪(Runnable)→ 阻塞(Blocked):竞争锁失败
  • 阻塞(Blocked)→ 就绪(Runnable):获得锁
  • 就绪(Runnable)→ 等待(Waiting):调用无超时等待方法
  • 等待(Waiting)→ 就绪(Runnable):被其他线程唤醒或中断
  • 就绪(Runnable)→ 超时等待(Timed Waiting):调用带超时等待方法
  • 超时等待(Timed Waiting)→ 就绪(Runnable):超时时间到或被唤醒 / 中断
  • 就绪(Runnable)→ 终止(Terminated):线程执行完毕或异常终止

NEW

java 复制代码
public class demo14 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("hello thread");
        });
        System.out.println(t.getState());
        t.start();
    }
}

RUNNABLE

java 复制代码
public class demo14 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {

            }
        });
        System.out.println(t.getState());//NEW
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());//RUNNABLE
    }
}

WAITING

java 复制代码
public class demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("helllo thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        t.join();
    }
}

此时 main 线程处于 waiting

TIMED_WAITING

java 复制代码
public class demo14 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("helllo thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

5.线程安全

5.1 线程安全的概念

如果多线程环境下代码运行的结果是符合预期的 , 即再单线程环境下应该的结果 , 则说这个程序是线程安全的

线程安全问题是指 : 当多个现场同时访问共享资源(如共享变量,文件,数据库连接等),由于线程回字形顺序的不确定性(CPU 调度的随机性),导致程序出现数据不一致,逻辑错误或异常的情况

5.2 观察线程不安全

java 复制代码
public class demo15 {
    private 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();
        t2.start();

        //如果此处没有这两个 join ,count为0,原因是main线程先执行打印了
        //join的作用是: 让主线程等待子线程执行完毕
        t1.join();
        t2.join();
        //预期结果应该是10w
        System.out.println(count);
    }
}

发现多次执行不但结果不一样 , 还不符合预期

对于 :

java 复制代码
t1.join();
t2.join();

这俩线程谁先 join();无所谓

情况 1 : t1 先结束 , t2 后结束

  1. main 先在 t1.join()阻塞等待
  2. t1 结束
  3. main 再在 t2.join()阻塞等待
  4. t2 结束
  5. main 继续执行后续打印

情况 2 : t2 先结束 , t1 后结束

  1. main 先在 t1.join()阻塞等待
  2. t2 结束 , t1.join()继续阻塞
  3. t1 结束
  4. main 执行到 t2.join() ; 但由于 t2 已经结束 , 此处不会阻塞
  5. main 继续执行后续打印

5.3 线程不安全的原因

1️⃣线程随机调度,抢占式执行(根本)

2️⃣多个线程同时修改同一个变量(修改共享数据)

3️⃣修改操作不是原子的(原子性缺失)(底层)

原子操作 : 不可分割的操作(如读取一个 int 变量),执行过程中不会被其他线程干扰

非原子操作:由多个步骤组成的操作(如 count++,实际就包括读取-更新-写回),若执行到一半就被其他线程抢占 CPU ,就可能导致数据错误

针对 cout++分两个线程同时执行 5w 次这个操作 , 有以下观点:

一条 JAVA 语句不一定是原子的,也不一定只是一条指令

每一次 count++操作都是由三步操作组成:① 从内存把数据读取到 CPU (load)② 进行数据更新(add)③ 把数据写回 CPU(save)

由于两个线程他们有各自不同的上下文;此时只有当一个线程的 save 完成后,在进行下一个线程的 load 才能线程安全

4️⃣内存可见性(底层)

可见性指:一个线程对共享变量值的修改,能够及时的被其他线程看到

换句话说 线程修改共享资源后,主内存的数据未能及时同步到其他线程的工作内存,导致其他线程读取到旧值

5️⃣指令重排序(底层)

CPU 指令重排序可能打乱代码执行顺序,多线程环境下引发逻辑错误

5.4 解决线程不安全问题(下文详细讲解)

1️⃣改为串行执行(解决多线程并发性执行的问题)

java 复制代码
public class demo15 {
    private 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();     
        //预期结果应该是10w
        System.out.println(count);
    }
}

2️⃣互斥锁(synchronized 关键字)

3️⃣使用线程安全的数据结构

4️⃣减少资源共享

5️⃣volatile 关键字

6.synchronized 关键字-监视器锁 monitor

6.1 synchronized 的特性

① 互斥

synchronized 会起到互斥的效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待

阻塞等待

针对每一把锁,操作系统内部维护了一个等待队列,当这个锁被某个线程占有时,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直到之前线程解锁后,有操作系统唤醒一个新的线程,再来获取到这个锁

  • 阻塞等到时不占用 CPU 资源,避免空耗
  • 需要依赖系统或其他线程唤醒,否则一直阻塞
java 复制代码
public class demo16 {

    private 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);
    }
}
  • 进入 synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块,相当于解锁

② 可重入

是锁的核心特性 , 指同一线程可以多次获取同一把锁 , 不会因为自身已持有该锁而陷入死锁 , 简单来说 "线程自己不会锁住自己"

底层实现 :

锁内部维护一个 线程持有计数器 和 当前持有线程引用:

线程首次获取锁 : 计数器设为 1 , 记录持有线程

同一线程再次获取锁 : 计数器+1 , 直接放行

线程释放锁 : 计数器-1 , 当计数器为 0 时 , 才释放锁给其他线程

6.2 synchronized 使用示例

synchronized 本质上要修改指定对象的对象头 , 从使用角度来看 , synchronized 也势必要搭配一个具体对象来使用

注意 :

  • 两个线程 针对同一个对象枷锁 , 才会产生互斥效果
  • 如果是不同的锁对象 , 此时不会产生互斥效果 , 线程也不安全

① 修饰代码块 : 明确指定锁哪个对象

锁任意对象
java 复制代码
public class demo17 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                synchronized (locker){
                    System.out.println("test t1");
                }
            }
        });
        t1.start();
        t1.join();
        System.out.println("test main");
    }
}

可以用任意对象来作为锁 ; 这个锁本身的类型并不重要 , 重要的是 : 是否有其他线程尝试 竞争这个锁 ;

实际上 把一个对象作为锁对象 , 并不影响对象本身的使用 ; 但是一般 一个对象只有一个作用

锁当前对象(容易出现问题)
java 复制代码
import static java.lang.Thread.sleep;

public class demo17 {
    private static int count = 0;
    public void method() throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (this) {
                    demo17.count++;
                }
            }
        });
        t1.start();
        //return t1;
    }

    public static void main(String[] args) throws InterruptedException {
        demo17 d3 = new demo17();
        d3.method();
//      d3.method();        
        demo17 d4 = new demo17();
        d4.method();

//        demo17 d1 = new demo17();
//
//        Thread T1 = d1.method();
//        T1.start();
//        T1.join();

//        Thread T2 = d1.method();
//        T2.start();
//        T2.join();

//        demo17 d2 = new demo17();
//        Thread T3 = d2.method();
//        T3.start();
//        T3.join();
        sleep(2000);
        System.out.println(demo17.count);
    }
}

也可以使用 Thread.currentThread()来替代 cur ; 但是这样写会让其他线程尝试竞争这个锁时 获取不到相同的锁对象

  • 若多个线程通过同一个实例对象调用 method() , 则会竞争 同一把锁 (this) , 此处 count++是安全的

  • 若多个线程通过不同实例对象调用 method() , 则每个线程的锁对象是不同的实例(this 不同) , 此时 锁不互斥 , count++会出现线程安全问题(因为 count 是静态共享的)

② 直接修饰普通方法 (和上面效果差不多)

java 复制代码
import static java.lang.Thread.sleep;

public class demo17 {
    private static int count = 0;
    public synchronized void method() throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {

                demo17.count++;

            }
        });
        t1.start();
        //return t1;
    }

    public static void main(String[] args) throws InterruptedException {
        demo17 d3 = new demo17();
        d3.method();
        //d3.method();

        demo17 d4 = new demo17();
        d4.method();
        sleep(2000);
        System.out.println(demo17.count);

        //        demo17 d1 = new demo17();
        //
        //        Thread T1 = d1.method();
        //        T1.start();
        //        T1.join();

        //        Thread T2 = d1.method();
        //        T2.start();
        //        T2.join();

        //        demo17 d2 = new demo17();
        //        Thread T3 = d2.method();
        //        T3.start();
        //        T3.join();

    }
}

③ 修饰静态方法

java 复制代码
public class StaticSyncDemo {
    // 静态共享变量(类级资源)
    private static int staticCount = 0;

    // 静态同步方法:锁对象是 StaticSyncDemo.class
    public synchronized static void increment() {
        staticCount++; // 安全修改静态变量
    }

    public static void main(String[] args) throws InterruptedException {
        // 两个线程调用静态同步方法
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                increment();
            }
        });

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

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

        System.out.println("staticCount = " + staticCount); // 一定是100000
    }
}

普通方法的锁和静态方法的锁的区别 :

普通方法的锁 : 仅对同一个实例对象的多线程生效 ; 若多个线程操作不同实例 ,锁不互斥 , 无法保证线程安全

静态方法的锁 : 对所有实例和线程生效 (因为类对象全局唯一) , 无论多少个实例 , 多线程调用静态同步方法时都会竞争同一把锁

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

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

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

还有一些线程不安全 , 使用一些机制锁来控制

  • Vector
  • HashTable
  • ConcurrentHashMap
  • StringBuffer

还有没有使用锁 ,但是不涉及修改操作 , 仍然线程安全

  • String
相关推荐
Chan162 小时前
Java 集合面试核心:ArrayList/LinkedList 底层数据结构,HashMap扩容机制详解
java·数据结构·spring boot·面试·intellij-idea
q***98522 小时前
Spring Boot(快速上手)
java·spring boot·后端
IT_Beijing_BIT2 小时前
Rust入门
开发语言·后端·rust
凌凌02 小时前
macOS安装SDKMAN
java
百***92022 小时前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(上)
java·spring boot·后端
青山的青衫2 小时前
【Java基础07】链表
java·开发语言·链表
麦麦鸡腿堡2 小时前
Java事件处理机制
java·开发语言·python
小画家~3 小时前
第二十八:golang Time.time 时间格式返回定义结构体
java·前端·golang
西岭千秋雪_3 小时前
Kafka安装和使用
java·分布式·kafka·java-zookeeper