多线程入门

多线程简介

1. 进程/线程

# 进程
- 进程由指令和数据组成,指令要运行,数据要读写,必须将指令加载到CPU,数据加载到内存。指令运行过程中还需要用到磁盘,网络等IO设备
- 进程用来加载指令,管理内存,管理IO
- 一个程序被运行,从磁盘加载这个程序的代码到内存,就是开启了一个进程
- 进程可以视为一个程序的实例
- 大部分程序可以运行多个实例进程,也有的程序只能启动一个实例进程

# 线程
- 一个进程内部包含1-n个线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- JVM中,进程作为资源分配的最小单元,线程作为最小调度单元

# 对比
- 进程基本上相互独立                                    线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间,供其内部的线程共享
- 进程间通信比较复杂: 同一台计算机的进程通信为IPC(Inter-process communication), 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,如HTTP
- 线程通信比较简单,因为它们共享进程内的内存,如多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般比进程上下文切换低

2. 并行/并发

2.1 并发

- 单核cpu下,线程是串行
- 任务调度器: 操作系统组件,将cpu的时间片(windows下为15ms)分给不同的线程使用
- cpu在线程间的切换非常快,感觉就是各个线程是同时运行的
- 微观串行,宏观并行

- concurrent:cpu核心同一个时间应对多个线程的能力

2.2 并行

- 多核cpu下,每个核都可以调度运行线程,这个时候线程就是并行的

- parrel: 同一个时间内,cpu真正去执行多个线程的能力
- 其实很多时候,并发和并行是同时存在的

在这里插入图片描述

3. 多线程应用场景

# 异步调用
- 某个方法的执行,不需要必须立刻获取返回结果
- 某个耗时业务,如果用另外一个线程来做,不会阻塞主线程的业务

# 提升效率
 - 一个任务,可以拆分为不同的任务,不同任务间互不依赖
- 多核cpu: 一个进程在进行多个独立操作时,没必要将其放到一个线程中顺序执行,创建多个线程分别执行,这样就会分到更多的cpu,执行更快
- 单核cpu: 没必要分成多个线程,因为依然会轮流执行,还会有上下文切换的损失
- java中要实现异步调用,必须采用多线程的方式

4. 创建线程

  • 启动JVM(main方法),就是开启了一个JVM进程
  • JVM进程内包含一个主线程,主线程可以派生出多个其他线程。同时还有一些守护线程,如垃圾回收线程
  • 主线程,守护线程,派生线程,cpu随机分配时间片,交替随机执行

4.1. 继承Thread类

  • 继承 Thread类,重写run(),start()启动线程
  • 两种写法:直接继承,匿名内部类
  • 在主线程内部开启了一个新的线程,在一个java进程中创造了一个其他线程
// 基本写法
package com.nike.erick.d01;

public class Demo01 {
    public static void main(String[] args) {
        ErickThread erickThread = new ErickThread();
        erickThread.start();

        System.out.println("Main Thread Running");
    }
}

class ErickThread extends Thread {

    @Override
    public void run() {
        System.out.println("Erick Thread Running");
    }
}

// 匿名内部类
package com.nike.erick.d01;

public class Demo02 {
    public static void main(String[] args) {
        Thread firstThread = new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ": Erick Thread");
            }
        };

        firstThread.start();
        System.out.println("Erick Main Running");
    }
}

4.2 实现Runnable接口

  • 将要启动的线程和要执行的任务分开,更加灵活
  • 实现Runnable接口,并将该对象作为Thread构造方法的参数传递
  • 三种方式: 直接实现,匿名内部类,lambda
- Runnable把线程和任务分开了
- Runnable更加容易和线程池相关的API结合使用
- Runnable让任务脱离了继承体系,更加灵活
- Runnable避免单继承的问题
// 实现Runnable接口
package com.nike.erick.d01;

public class Demo03 {
    public static void main(String[] args) {
        Thread firstThread = new Thread(new LucyThread(), "t1");
        firstThread.start();
        System.out.println("Main Thread Running");
    }
}

class LucyThread implements Runnable {

    @Override
    public void run() {
        System.out.println("Lucy Thread Running");
    }
}
// 匿名内部类
package com.nike.erick.d01;

public class Demo04 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Lucy Thread");
            }
        }, "t1");

        thread.start();

        System.out.println("Main Thread");
    }
}
// Lambda 方法
package com.nike.erick.d01;

public class Demo04 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("Lucy Thread"), "t1");

        thread.start();

        System.out.println("Main Thread");
    }
}

Runnable vs Thread

Runnable
# 策略模式
- 实际执行时候是调用的Runnable接口的run方法
- 因为将Runnable实现类传递到了Thread的构造参数里面
  • Runnable接口
// @FunctionalInterface修饰的接口,可以用lambda来创建
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • Thread 类
public class Thread implements Runnable {

    private Runnable target;
    
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}
Thread
  • Thread的实现类重写了run方法,因此在通过Thread的start方法调用的时候,实际是调用了实现类的run方法

4.3 FutureTask接口

  • 可以获取任务执行结果的一个接口
# FutureTask 继承关系
class FutureTask<V> implements RunnableFuture<V>
interface RunnableFuture<V> extends Runnable, Future<V>
interface Future<V> :
                       boolean cancel()
                       boolean isCancelled()
                       boolean isDone()
                       V get()
                       V get(long timeout, TimeUnit unit)

# Callable 接口实现类
interface Callable<V>
package com.nike.erick.d01;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class Demo06 {
    public static void main(String[] args) {

        FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("slave-thread running");
                TimeUnit.SECONDS.sleep(2);
                return "success from erick";
            }
        });

        Thread thread = new Thread(futureTask, "erick-thread");
        thread.start();

        try {
            /*获取结果的时候,会将主线程阻塞*/
            System.out.println("slave-thread result: " + futureTask.get());
            System.out.println("slave-thread result: " + futureTask.isCancelled());
            System.out.println("slave-thread result: " + futureTask.isDone());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("Main Thread ending");
    }
}

5. 进程查看

5.1. Linux

# process status
ps -ef
ps -fe               # 进程查看

ps -ef|grep java
ps -fe|grep java     # 管道运算符, 查看java进程(包含java进程和查看的grep进程)

kill pid             # 杀死指定进程
top                  # 动态查看当前进程的占用cpu和mem情况。ctrl+c 退出

top -H -P (pid)      # 查看某个进程内部的所有线程的cpu使用
                     # -H : 表示查看线程

5.2 Java

JPS

5.3 线程工具JConsole

  • Java内置,检测java线程的图形化界面工具
  • 位于jdk/bin/目录下
  • 直接在Mac控制台输入: jconsole即可打开
  • 可以用来检测死锁

6. 主线程和守护线程

  • main方法启动后,就会开启一个JVM, 包含一个主线程 和 守护线程, 其他线程
  • JVM就是一个java进程
  • 守护线程: 只要其他非守护线程运行结束了,即使守护线程的代码没执行完,也会强制退出
  • 垃圾回收器就是一种守护线程
package com.nike.erick.d01;

import java.util.concurrent.TimeUnit;

/* 输出结果: Dame Starting
            Main Thread ending*/
public class Demo08 {
    public static void main(String[] args) throws InterruptedException {
        Thread daemonThread = new Thread(() -> {
            try {
                System.out.println("Dame Starting");
                TimeUnit.SECONDS.sleep(10);
                System.out.println("Dame");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        daemonThread.setDaemon(true);
        daemonThread.start();

        TimeUnit.SECONDS.sleep(2);
        System.out.println("Main Thread ending");
    }
}

7. JVM 模型

7.1 栈与栈帧

  • Java Virtual Machine Stacks: Java虚拟机栈内存
  • 栈内存:虚拟机启动后,每个线程启动后,都会分配一块独立的栈内存
  • 栈帧: 每个栈包含多个栈帧(Frames),对应着每次方法调用时所占的内存
  • 活动栈帧:每个线程只能有一个活动栈帧,对应着当前正在执行的方法
  • 弹栈:方法执行完毕后,栈帧内存依次弹栈,栈内存销毁

在这里插入图片描述

7.2 Thread Context Switch

  • 因为一些原因导致cpu不再执行当前线程,转而执行另一个线程代码
1. 线程的cpu时间片用完
2. 垃圾回收:               # 垃圾回收的时候,其他所有线程要暂停, STW
3. 有更高优先级的线程需要运行
4. 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
  • 当发生线程上下文切换时候,需要操作系统保存当前线程的状态,并恢复另一个线程的状态
  • 程序计数器:java中的, 线程切换时,记住下一条jvm指令的执行地址,是线程私有的
  • 状态包含:程序计数器,虚拟机中每个栈帧的信息,操作数栈,返回地址等
  • Thread Context Switch频繁发生会影响性能

线程方法

1. start/run

public synchronized void start()
public void run()

1. start() :  
       1.1 线程从new状态转换为runnable状态,等待cpu分片从而有机会转换到running状态
       1.2 在主线程之外,再开启了一个线程
       1.3 已经start的线程,不能再次start  “IllegalThreadStateException”
       
2. run():    
      2.1 线程内部调用start后实际执行的一个普通方法
      2.2 如果线程直接调用run() ,只是在主线程内,调用了一个普通的方法

2. sleep/yield

2.1 sleep

public static native void sleep(long millis) throws InterruptedException

1. 线程放弃cpu,从RUNNABLE 进入 TIMED_WAITING状态
2. 睡眠中的线程可以自己去打断自己的休眠
3. 不会放弃当前的锁资源
4. 睡眠结束后,会变为RUNNABLE,并不会立即执行,而是等待cpu时间片的分配
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("slave-thread running");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("slave-thread before start: " + thread.getState()); //NEW

        thread.start();
        System.out.println("slave-thread after start: " + thread.getState()); // RUNNABLE

        TimeUnit.SECONDS.sleep(2);
        System.out.println("slave-thread while sleeping: " + thread.getState()); // TIMED_WAITING
    }
}
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("slave-thread running");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    System.out.println("slave-thread interrupted");
                    throw new RuntimeException("Interrupted Exception");
                }
                System.out.println("slave-thread ending"); // 不会执行
            }
        });

        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt(); // 打断睡眠中的线程
    }
}

2.2 yield

public static native void yield()

- 线程让出cpu资源,让其他线程先去执行
- 让线程从RUNNING状态转换为RUNNABLE状态
- 假如其他线程不用cpu,那么cpu又会分配时间片到当前线程,可能压根就没停下

2.3 区别

sleep: 让当前线程从 RUNNING --> Timed Waiting(阻塞), 睡眠结束后会进入 RUNNABLE状态
yield: 让当前线程从 RUNNING --> RUNNABLE 状态,然后调度其他线程
priority及yield
package com.nike.erick.d02;

public class Demo03 {
    public static void main(String[] args) {
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (true) {
                    i++;
                    System.out.println("T1--->" + i);
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (true) {
                Thread.yield();// 可以放弃当前线程
                    i++;
                    System.out.println("        T2--->" + i);
                }
            }
        });

        firstThread.setPriority(10);
        firstThread.start();
        secondThread.setPriority(1);
        secondThread.start();
    }
}
sleep应用
  • 程序一直执行,浪费cpu资源
package com.dreamer.multithread.day02;

public class Demo01 {
    public static void main(String[] args) {
        while (true) {
            System.out.println("i am working");
        }
    }
}
  • 程序间歇性休眠,让出cpu资源, 避免空转
package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("i am working");
        }
    }
}

3. Join

  • 谁调用 join,就等谁的线程结束后再去运行当前线程

3.1. join()

  • 等待当前线程执行完毕
public final void join() throws InterruptedException
单个线程
package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    static int number = 0;

    public static void main(String[] args) {
        Thread slaveThread = new Thread("t1") {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                number = 10;
            }
        };

        slaveThread.start();
        
        // t1线程调用join,t1线程就是插队执行
        try {
            slaveThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("number: " + number);
    }
}
多个线程
package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo03 {
    public static void main(String[] args) throws InterruptedException {
        Thread firstThread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread secondThread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        firstThread.start();
        secondThread.start();

        /**
         * 1. 两个线程同时插队,以相同优先级执行
         * 2. 所以一共等待时间为2s
         */
        long begin = System.currentTimeMillis();
        firstThread.join();
        secondThread.join();
        long end = System.currentTimeMillis();

        System.out.println("total time consuming: " + (end - begin));
    }
}

3.2 join(long millis)

  • 有时效的等待:最多等待多少ms, 0 代表永远执行完毕
  • 假如线程join的等待时间超过了实际执行时间,执行完后就可以不用继续等了
public final synchronized void join(long millis) throws InterruptedException
package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    static int number = 0;

    public static void main(String[] args) {
        Thread slaveThread = new Thread("t1") {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                number = 10;
            }
        };

        slaveThread.start();

        // t1线程调用join,t1最多插队1秒,然后就继续执行当前线程
        try {
            slaveThread.join(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 0
        System.out.println("number: " + number);
    }
}

3.3 应用

  • 同步等待其他线程的结果调用

4. interrupt

# Thread 类

# 1. 打断线程,谁调用打断谁
public void interrupt()

# 2. 判断线程是否被打断 : 默认值为true
         #  不会清除打断标记
public boolean isInterrupted()

         #  会将打断标记置为false
public static boolean interrupted()

4.1 打断阻塞线程

  • 打断线程,抛出异常,并将线程打断标记重置为false(需要一点缓冲时间)
  • 如sleep,join,wait的线程,被打断的线程抛出错误
package com.erick.multithread.d1;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread slaveThread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        slaveThread.start();

        TimeUnit.SECONDS.sleep(3);

        // false
        System.out.println(slaveThread.isInterrupted());
        slaveThread.interrupt();
        // 留下一点缓冲时间
        TimeUnit.SECONDS.sleep(3);
        // 阻塞的线程,被打断后,后通过异常的方式抛出,并将打断标记重制为false
        // false
        System.out.println(slaveThread.isInterrupted());
    }
}

4. 2 打断正常线程

普通打断
  • 正常线程运行时,收到打断, 打断信号变为true,但不会做任何处理
  • 并不会直接终止打断线程,而是发出打断线程的请求
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo05 {
    public static void main(String[] args) throws InterruptedException {
        Thread slaveThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("slave-thread running");
                }
            }
        });

        slaveThread.start();
        TimeUnit.SECONDS.sleep(1);

        /*正常运行的线程,被打断后
         * 1. 继续正常运行
         * 2. 将打断标记置为 true*/
        slaveThread.interrupt();
    }
}
通过打断标记
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo05 {
    public static void main(String[] args) throws InterruptedException {
        Thread slaveThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    /*判断打断标记*/
                    if (Thread.currentThread().isInterrupted()) {
                        break;
                    }
                    System.out.println("slave-thread running");
                }
            }
        });

        slaveThread.start();
        TimeUnit.SECONDS.sleep(1);

        /*正常运行的线程,被打断后
         * 1. 继续正常运行
         * 2. 将打断标记置为 true*/
        slaveThread.interrupt();
    }
}

Two Phase Termination

介绍
  • 终止正常执行的线程的优雅的模式:留下料理后事的机会
# 1. 错误思路一:
stop()   已经被废弃
   stop会真正kill线程,如果这时线程锁住了共享资源,
    那么当该线程被杀死后,再也没有机会去释放锁,其他线程将永远无法获取锁
# 2. 错误思路二: 
System.exit()
   目的仅仅是停止一个线程,但这种做法会将当前线程所在的整个java程序终止
业务场景
  • 监视线程,每隔2秒去执行监视流程,如果被打断,则中止监视流程
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo06 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        // 料理后事
                        closeJob();
                        break;
                    }

                    /*阶段一: 业务代码: 可能被打断*/
                    try {
                        executeBusiness();
                    } catch (Exception e) {
                        continue;
                    }

                    /*阶段二:休眠操作: 也可能被打断*/
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        System.out.println("休眠时候被打断了: " + Thread.currentThread().isInterrupted());
                        // 如果休眠时被打断了,那么打断标记就变为了false, 需要再次打断,重制标记为为true
                        // 这里就类似于打断正常线程
                        Thread.currentThread().interrupt();
                    }
                }
            }
        });

        thread.start();

        TimeUnit.SECONDS.sleep(4);
        thread.interrupt();
    }

    private static void executeBusiness() {
        System.out.println("执行了业务");
    }

    private static void closeJob() {
        System.out.println("料理后事");
    }
}

4.3 打断park线程

  • LockSupport: public static void park()
  • park当前的线程: 线程一直生存,直到被打断才会继续往下执行
  • 被打断后,打断标记就会变为true,就不能二次park了
单次park
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class Demo07 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread start running....");

                // 将当前线程停下来
                LockSupport.park();
                System.out.println("after first park...");
            }
        });

        thread.start();
        TimeUnit.SECONDS.sleep(2);
        // 打断后就会继续执行
        thread.interrupt();
    }
}
多次park
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class Demo08 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("slave thread running");
                LockSupport.park();
                System.out.println("after first park...");

                // 获取当前线程的打断标记,同时将打断标记清除,即为 false
                System.out.println("打断标记:" + Thread.interrupted());

                LockSupport.park(); // 再次park
                System.out.println("after second park...");
            }
        });

        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        TimeUnit.SECONDS.sleep(3);
        thread.interrupt();
    }
}

5. 其他方法

### 1. 成员方法

# 1. 名字
public final synchronized void setName(String name)  
public final String getName()

# 2. 优先级:最小为1,最大为10,默认为5
#    只是一个cpu执行目标线程的顺序  建议设置,任务调度器可以忽略它进行分配资源
public final void setPriority(int newPriority)
public final int getPriority()

# 3. 线程id: 13
public long getId()

# 4. 是否存活
public final native boolean isAlive()

# 5. 是否后台线程
public final boolean isDaemon()

# 6. 获取线程的状态
public State getState()
NEW  RUNNABLE  BLOCKED  WAITING   TIMED_WAITING   TERMINATED


### 2. 静态方法
# 1. 获取当前线程
public static native Thread currentThread()


## 3. 过时方法
- stop:停止线程运行
- suspend: 让线程暂停使用
- resume: 恢复线程运行
- 不推荐理由: 这三种方法,都可能造成死锁问题

生命周期

1. 操作系统

  • 从操作系统层面来说,包含五种状态
  • cpu的时间片只会分给可运行状态的线程,阻塞状态的需要其本身阻塞结束

image-20220925095436125

NEW

  • new 出一个Thread的对象时,线程并没有创建,只是一个普通的对象
  • 调用start,new 状态进入runnable状态

RUNNABLE

  • 调用start后,在jvm中创建了新的线程,但并不立即执行,只是处于就绪状态,即有资格执行
  • 等待cpu分配权限,只有轮到它的时候,才会真正执行

RUNNING

  • 一旦cpu调度到了该线程,该线程才会真正开始执行
# 该状态的线程可以转换为其他状态
1. 进入TERMINATED:   stop(不推荐), 线程执行完毕, jvm crash
2. 进入BLOCK:        sleep, wait,阻塞IO如网络数据读写, 获取某个锁资源
3. 进入RUNNABLE:     cpu轮询到其他线程,线程主动yield放弃cpu

BLOCK

# 该状态的线程可以转换为其他状态
1. 进入TERMINATED:   stop(不推荐), jvm crash
2. 进入RUNNABLE:     线程阻塞结束,完成了指定时间的休眠
                     wait中的线程被其他线程notify/notifyall
                     获取到了锁资源

TERMINATED

  • 线程正常结束
  • 线程运行出错意外结束
  • jvm crash,导致所有的线程都结束

2. JAVA层面

  • 根据Thread类中内部State枚举,分为六种状态

线程安全

1. 线程不安全

1.1 共享变量

  • 多个线程对共享变量的并发修改,导致结果并不像预期中那样
package com.nike.erick.d03;

public class Demo01 {
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    number++;
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    number--;
                }
            }
        });

        firstThread.start();
        secondThread.start();

        firstThread.join();
        secondThread.join();

        System.out.println("number: " + number);
    }
}

1.2 原因

字节码指令
  • 线程拥有自己的栈内存,读数据时会从主内存中拿,写完后会将数据写回主存
  • i++在实际执行的时候,是一系列指令,一系列指令就会导致指令交错

在这里插入图片描述

指令交错
  • 指令交错存在于多线程之间
  • 线程上下文切换,引发不同线程内指令交错,最终导致上述操作结果不会为0

在这里插入图片描述

概念
  • 线程不安全: 只会存在于多个线程共享的资源
  • 临界区: 对共享资源的多线程读写操作的代码块
  • 竞态条件: 多个线程在在临界区内,由于代码的指令交错,对共享变量资源的争抢
多线程  读    共享资源  没问题
多线程  读写  共享资源  可能线程不安全(指令交错)

2. Synchronized

  • 一种阻塞式解决线程安全问题的方案

2.1 同步代码块

基本语法
  • 对象锁,只要为同一个对象,为任意对象(除基本类型)
  • 对象锁:尽可能用final修饰,这样保证对象的引用不可变,从而确保是同一把锁
- synchronized: 一种阻塞式的,用来解决多线程访问共享资源引发的不安全的解决方案
- synchronized: 可在不同代码粒度进行控制
- synchronized: 保证了《临界区代码的原子性(字节码)》,不会因为线程的上下文切换而被打断
- synchronized: 必须保证对对同一个对象加锁(Integer.value(0))
package com.nike.erick.d03;

public class Demo01 {
    private static int number = 0;

    /*任何对象都可以,只要保证是引用类型*/
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        /*粗粒度的锁
          细粒度的锁:也可以加在每个for循环中*/
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 100000; i++) {
                        number++;
                    }
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 100000; i++) {
                        number--;
                    }
                }
            }
        });

        firstThread.start();
        secondThread.start();

        firstThread.join();
        secondThread.join();

        System.out.println("number: " + number);
    }
}
原理
1. a线程获取锁,执行代码
2. 其他线程这个时候要进入,无法获取锁资源,就会被block,进入  《等待队列》
   同时进入上下文切换
   
3. a线程执行完毕后,释放锁资源。唤醒其他线程,进行cpu的争夺
面向对象改进
package com.nike.erick.d03;

public class Demo03 {
    private static Room room = new Room();

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

        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    room.decrement();
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    room.increment();
                }
            }
        });

        firstThread.start();
        secondThread.start();
        firstThread.join();
        secondThread.join();

        System.out.println("result:" + room.getValue());
    }
}

class Room {
    private int counter;

    // 锁对象一般用当前对象
    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    public void decrement() {
        synchronized (this) {
            counter--;
        }
    }

    public int getValue() {
        synchronized (this) {
            return counter;
        }
    }
}

2.2 同步方法

成员方法
  • 同步成员方法和同步代码块效果一样,必须保证同步代码块的锁对象是this对象
  • 可能锁粒度不太一样
  • 同步方法的锁对象是this,即当前对象
@Data
class Calculator {
    private int number;

    public void incr() {
        synchronized (this) {
            for (int i = 0; i < 10000; i++) {
                number++;
            }
        }
    }
    // 同步方法
    public synchronized void decr() {
        for (int i = 0; i < 10000; i++) {
            number--;
        }
    }
}
静态成员方法
  • 锁对象:锁用的是类的字节码对象: Calculator.class
@Data
class Calculator {
    private static int number;

    public int getNumber() {
        return number;
    }

    public static void incr() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Calculator.class) {
                number++;
            }
        }
    }

    public static synchronized void decr() {
        for (int i = 0; i < 10000; i++) {
            number--;
        }
    }
}
锁对象
- 同步代码块,必须保证用的锁是同一个对象,但是不能为基本数据类型
- 同步成员方法的锁对象是this
- 同步静态成员方法的锁对象是当前类的字节码对象     .class
- 如果多个线程对共享变量读写,但是部分线程没有加锁保护,依然线程不安全

3. 线程安全场景

3.1 成员变量/静态成员变量

1. 没被多线程共享:        则线程安全
2. 被多线程共享:
     2.1 如果只读,   则线程安全
     2.2 如果有读写, 则可能发生线程不安全问题

3.2 局部变量

线程安全
# 每个线程的方法都会创建单独的栈内存,局部变量保存在自己当前方法的栈桢内
# 局部变量线程私有

1. 局部变量是基础数据类型时: 是线程安全的
2. 但局部变量是应用类型时:   可能不安全
  2.1 如果该对象没有逃离方法的作用访问,则线程安全
  2.2 如果该对象逃离方法的作用范围,则可能线程不安全 《引用逃离》
 
# 避免线程安全类变为不安全类: 不要让一个类的方法被重写
- final修饰禁止继承,或对可能引起安全的方法加private
引用逃离
  • 如果一个类不是final类,那么就可能被继承
  • 被继承的时候发生方法覆盖,覆盖方法如果创建新的线程,就可能发生局部变量不安全
// 安全
package com.nike.erick.d01;

import java.util.ArrayList;
import java.util.List;

public class Demo05 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        SafeCounter safeCounter = new SafeCounter();
        safeCounter.operation(list);
        System.out.println(list);
    }
}

class SafeCounter {
    public void operation(List<String> list) {
        for (int i = 0; i < 10000; i++) {
            addElement(list);
            deleteElement(list);
        }
    }

    public void addElement(List<String> list) {
        list.add("HELLO");
    }

    public void deleteElement(List<String> list) {
        list.remove(0); // 移除元素
    }
}
// 不安全
package com.nike.erick.d01;

import java.util.ArrayList;
import java.util.List;

public class Demo05 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        UnsafeCounter unsafeCounter = new UnsafeCounter();
        unsafeCounter.operation(list);
        System.out.println(list);
    }
}

class SafeCounter {
    public void operation(List<String> list) {
        for (int i = 0; i < 10000; i++) {
            addElement(list);
            deleteElement(list);
        }
    }

    public void addElement(List<String> list) {
        list.add("HELLO");
    }

    public void deleteElement(List<String> list) {
        list.remove(0); // 移除元素
    }
}

class UnsafeCounter extends SafeCounter {
    @Override
    public void deleteElement(List<String> list) {
        /*开启了新的线程来改变*/
        // index out of bound
        // 相当于把删除的操作延迟或提前了
        new Thread(() -> list.remove(0)).start();
    }
}

3.3 线程安全类

JDK类
  • 多个线程同时调用他们同一个实例的方法时,线程安全
  • 线程安全类中的方法的组合,不一定线程安全
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
package com.nike.erick.d03;

import java.util.Hashtable;
import java.util.concurrent.TimeUnit;

public class Demo04 {

    /*共享资源*/
    private static Hashtable<String, String> hashtable = new Hashtable<>();

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

        Thread firstThread = new Thread(() -> combinedMethod(hashtable));

        Thread secondThread = new Thread(() -> combinedMethod(hashtable));

        firstThread.start();
        secondThread.start();
        firstThread.join();
        secondThread.join();
        System.out.println(hashtable.size());
    }

    // 方法的组合不是 线程安全的
    // 如果要线程安全,必须将组合方法也设置成 synchronized
    public static void combinedMethod(Hashtable<String, String> hashtable) {
        if (null == hashtable.get("name")) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            hashtable.put("name", "erick");
            System.out.println(Thread.currentThread().getName());
            hashtable.put(Thread.currentThread().getName(), "00");
        }
    }
}
不可变类
  • 类中属性都是final,不能修改
  • 如 String,Integer
#  实现线程安全类的问题
- 无共享变量
- 共享变量不可变
- synchronized互斥修饰
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值