多线程简介
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的构造参数里面
// @FunctionalInterface修饰的接口,可以用lambda来创建
@FunctionalInterface
public interface Runnable {public abstract void run();
}
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应用
package com.dreamer.multithread.day02;public class Demo01 {public static void main(String[] args) {while (true) {System.out.println("i am working");}}
}
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的时间片只会分给可运行状态的线程,阻塞状态的需要其本身阻塞结束
NEW
new 出一个Thread的对象时,线程并没有创建,只是一个普通的对象 调用start,new 状态进入runnable状态
RUNNABLE
调用start后,在jvm中创建了新的线程,但并不立即执行,只是处于就绪状态,即有资格执行 等待cpu分配权限,只有轮到它的时候,才会真正执行
RUNNING
# 该状态的线程可以转换为其他状态
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互斥修饰