多线程入门
创始人
2024-02-02 15:34:41

多线程简介

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 {@Overridepublic 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() {@Overridepublic 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 {@Overridepublic 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() {@Overridepublic 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);}@Overridepublic void run() {if (target != null) {target.run();}}
}

Thread

  • Thread的实现类重写了run方法,因此在通过Thread的start方法调用的时候,实际是调用了实现类的run方法

4.3 FutureTask接口

  • 可以获取任务执行结果的一个接口
# FutureTask 继承关系
class FutureTask implements RunnableFuture
interface RunnableFuture extends Runnable, Future
interface Future :boolean cancel()boolean isCancelled()boolean isDone()V get()V get(long timeout, TimeUnit unit)# Callable 接口实现类
interface Callable
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 futureTask = new FutureTask<>(new Callable() {@Overridepublic 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 StartingMain 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 InterruptedException1. 线程放弃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() {@Overridepublic 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()); //NEWthread.start();System.out.println("slave-thread after start: " + thread.getState()); // RUNNABLETimeUnit.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() {@Overridepublic 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() {@Overridepublic void run() {int i = 0;while (true) {i++;System.out.println("T1--->" + i);}}});Thread secondThread = new Thread(new Runnable() {@Overridepublic 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") {@Overridepublic 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") {@Overridepublic 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();}// 0System.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);// falseSystem.out.println(slaveThread.isInterrupted());slaveThread.interrupt();// 留下一点缓冲时间TimeUnit.SECONDS.sleep(3);// 阻塞的线程,被打断后,后通过异常的方式抛出,并将打断标记重制为false// falseSystem.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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void run() {System.out.println("slave thread running");LockSupport.park();System.out.println("after first park...");// 获取当前线程的打断标记,同时将打断标记清除,即为 falseSystem.out.println("打断标记:" + Thread.interrupted());LockSupport.park(); // 再次parkSystem.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() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {number++;}}});Thread secondThread = new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic void run() {synchronized (lock) {for (int i = 0; i < 100000; i++) {number++;}}}});Thread secondThread = new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {room.decrement();}}});Thread secondThread = new Thread(new Runnable() {@Overridepublic 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 list = new ArrayList<>();SafeCounter safeCounter = new SafeCounter();safeCounter.operation(list);System.out.println(list);}
}class SafeCounter {public void operation(List list) {for (int i = 0; i < 10000; i++) {addElement(list);deleteElement(list);}}public void addElement(List list) {list.add("HELLO");}public void deleteElement(List 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 list = new ArrayList<>();UnsafeCounter unsafeCounter = new UnsafeCounter();unsafeCounter.operation(list);System.out.println(list);}
}class SafeCounter {public void operation(List list) {for (int i = 0; i < 10000; i++) {addElement(list);deleteElement(list);}}public void addElement(List list) {list.add("HELLO");}public void deleteElement(List list) {list.remove(0); // 移除元素}
}class UnsafeCounter extends SafeCounter {@Overridepublic void deleteElement(List 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 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());}// 方法的组合不是 线程安全的// 如果要线程安全,必须将组合方法也设置成 synchronizedpublic static void combinedMethod(Hashtable 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互斥修饰

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...