1. 线程概述
进程: 进程就是在操作系统中运行的程序
线程: 线程就是进程的一个执行单元, 或者一条执行路径
主线程: JVM启动主线程,主线程运行main方法
用户线程: 用户开启新的线程,也称子线程
守护线程: 守护线程是为其他线程提供服务的线程,也叫后台线程. JVM中垃圾回收器就是一个守护线程。守护线程不能单独运行, 当JVM中只有守护线程时, JVM会退出。
- 启动迅雷应用程序,就是打开一个进程,在该软件中可以同时下载多部电影,每部电影的下载就是一个线程。
- 对于Java程序来说,当在DOS命令窗口中输入
java HelloWorld
回车之后,会先启动JVM,JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收器负责看护,回收垃圾。至此,一个Java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。 - 一个进程至少有一个线程,如果这个进程有多个线程,称该进程为多线程应用程序。
- 进程中的多个线程是相互独立的,每个线程都有它自己的线程栈。
注意:
- 进程A和进程B的内存独立不共享
- 线程A和线程B的堆内存和方法区内存共享。但是栈内存独立,一个线程对应一个栈
- 假设启动10个线程,就会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发
- 火车站可以看作是一个进程。火车站中的每一个售票窗口可以看作是一个线程。我在窗口1购票,你在窗口2购票,你不需要等我,我也不需要等你。因此多线程并发可以提高效率
- java中之所以有多线程机制,目的就是为了提高程序的处理效率
- 使用多线程机制后,main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈
问题:
对于单核CPU来说,真的可以做到真正的多线程并发吗?
- 对于多核的CPU来说,真正的多线程是没有问题的。4核CPU表示在同一时间点上,可以真正有4个进程并发执行
- 单核的CPU表示只有一个大脑,不能够做到真正的多线程并发,但是可以给人一种多线程并发的感觉。对于单核CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情同时在做!
2. Java创建线程的方式
实现线程的方式有三种:
-
编写一个类,直接继承 java.lang.Tread,重写 run 方法
package com.thread; /* 实现线程的第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法 */ public class ThreadTest { // 这里是main方法,这里的代码属于主线程,在主栈中运行 public static void main(String[] args) { // 新建一个分支线程对象 MyThread myThread = new MyThread(); // 启动线程 // start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了 // 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程就启动成功了 // 启动成功的线程会自动调用run方法,并且run方法在分支栈的底部(压栈) // run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的 myThread.start(); // 下面的的代码还是运行在主线程中的 for (int i = 0;i < 1000;i++) { System.out.println("主线程——》" + i); } } } class MyThread extends Thread { // 如何快捷生成重写方法呢? // 按住“Alt+Insert”,输入“o”,找到“Override”,输入“run”,找到“run”方法 @Override public void run() { // 编写程序,这段程序运行在分支线程中(分支栈) for (int i = 0;i < 1000;i++) { System.out.println("分支线程——》" + i); } } } ------------------------------------------------------ 部分结果展示: 分支线程——》8 主线程——》8 分支线程——》9 主线程——》9 分支线程——》10 主线程——》10 分支线程——》11 主线程——》11 分支线程——》12 主线程——》12 分支线程——》13 主线程——》13 分支线程——》14 分支线程——》15 主线程——》14 分支线程——》16 主线程——》15 分支线程——》17 主线程——》16 分支线程——》18 主线程——》17 分支线程——》19 主线程——》18 分支线程——》20 主线程——》19 分支线程——》21
-
编写一个类,实现 java.lang.Runnable 接口,实现 run 方法【常用】
package com.thread; /* 实现线程的第二种方式:编写一个类,实现java.lang.Runnable接口 */ public class ThreadTest { // 这里是main方法,这里的代码属于主线程,在主栈中运行 public static void main(String[] args) { // 创建一个可运行的对象 MyRunnable r = new MyRunnable(); // 将可运行的对象封装成一个线程对象 Thread t = new Thread(r); // 启动线程 t.start(); // 下面的的代码还是运行在主线程中的 for (int i = 0;i < 1000;i++) { System.out.println("主线程——》" + i); } } } // 这并不是一个线程类,它是一个可运行的类,他还不是一个线程 class MyRunnable implements Runnable { @Override public void run() { // 编写程序,这段程序运行在分支线程中(分支栈) for (int i = 0;i < 1000;i++) { System.out.println("分支线程——》" + i); } } } ------------------------------------------------------ 部分结果展示: 分支线程——》96 主线程——》100 分支线程——》97 主线程——》101 分支线程——》98 主线程——》102 分支线程——》99 主线程——》103 分支线程——》100 主线程——》104 分支线程——》101 主线程——》105 分支线程——》102 主线程——》106 分支线程——》103 主线程——》107 分支线程——》104 主线程——》108 分支线程——》105 主线程——》109 主线程——》110 主线程——》111
上面的方式也可以通过一个匿名内部类的方式实现:
package com.thread; /* 通过匿名内部类可以实现吗? */ public class ThreadTest { // 这里是main方法,这里的代码属于主线程,在主栈中运行 public static void main(String[] args) { // 创建线程对象,采用匿名内部类方式 // 这是通过一个没有名字的类吗,new出来的对象 Thread t = new Thread(new Runnable() { @Override public void run() { // 编写程序,这段程序运行在分支线程中(分支栈) for (int i = 0;i < 1000;i++) { System.out.println("分支线程——》" + i); } } }); // 启动线程 t.start(); // 下面的的代码还是运行在主线程中的 for (int i = 0;i < 1000;i++) { System.out.println("主线程——》" + i); } } } ------------------------------------------------------ 部分结果展示: 分支线程——》898 分支线程——》899 分支线程——》900 分支线程——》901 主线程——》261 分支线程——》902 分支线程——》903 主线程——》262 主线程——》263 主线程——》264 主线程——》265 主线程——》266 主线程——》267 主线程——》268 主线程——》269 主线程——》270 主线程——》271 分支线程——》904 主线程——》272 主线程——》273 主线程——》274 主线程——》275 主线程——》276 主线程——》277 主线程——》278 主线程——》279 主线程——》280 主线程——》281 主线程——》282
-
定义Callable接口的实现类(JDK8新特性)
优点:这种方式实现的线程可以获取线程的返回值。之前的两种方式是无法获取线程返回值的,因为run方法返回void。
缺点:效率较低,在获取t线程执行结果的时候,当前下线程受阻塞。package com.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /* 实现线程的第三种方式:实现Callable接口 */ public class ThreadTest { // 这里是main方法,这里的代码属于主线程,在主栈中运行 public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建线程对象 MyRunnable myRunnable = new MyRunnable(); // 创建Callable接口的实现类对象 FutureTask<Integer> task = new FutureTask<>(myRunnable); // 创建FutureTask对象 // FutureTask类实现了RunnableFuture接口, 该接口继承了Runnable接口, FutureTask类就是Runnable接口的实现类 Thread t3 = new Thread(task); // 开启线程 t3.start(); // 当前线程是main线程 for (int i = 1; i <= 100; i++) { System.out.println("main : " + i); } // 在main线程中可以取得子线程的返回值 // 在这里,main方法可能会受到阻塞,因为main方法需要等待get方法获得到的返回值,而get方法可能需要很久,因此在等待的过程中,main方法阻塞 System.out.println(" task result : " + task.get() ); } } // 定义类实现Callable接口 // Callable接口的call()方法有返回值, 可以通过Callable接口泛型指定call()方法的返回值类型 class MyRunnable implements Callable<Integer> { // 重写call()方法, call()方法中的代码就是子线程要执行的代码 @Override public Integer call() throws Exception { //累加1~100之间的整数和 int sum = 0 ; for(int i = 1; i<=100; i++){ sum += i; System.out.println("sum=" + sum); } return sum; } } ------------------------------------------------------ 部分结果展示: main : 75 sum=2926 main : 76 sum=3003 main : 77 sum=3081 main : 78 sum=3160 main : 79 sum=3240 main : 80 sum=3321 main : 81 sum=3403 main : 82 sum=3486 main : 83 sum=3570 main : 84 sum=3655 main : 85 sum=3741 main : 86 sum=3828 main : 87 sum=3916 main : 88 sum=4005
注意:
第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更加灵活。
3. 线程生命周期
- 就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权利(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态
- run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢夺到CPU时间之后,会重新进入run方法接着上一次的代码继续往下执行
- 当一个线程遇到阻塞事件,例如接收到用户键盘输入,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片
4. 线程对象的相关操作
4.1. 获取线程对象的名字
方法: 通过getName()
方法可以获取线程对象的名字。线程对象.getName();
代码:
package com.thread;
public class ThreadTest {
// 这里是main方法,这里的代码属于主线程,在主栈中运行
public static void main(String[] args) {
// 创建线程对象0
Mythread mythread0 = new Mythread();
// 创建线程对象1
Mythread mythread1 = new Mythread();
// 创建线程对象2
Mythread mythread2 = new Mythread();
// 获取线程对象的名字0
String tName0 = mythread0.getName();
// 获取线程对象的名字1
String tName1 = mythread1.getName();
// 获取线程对象的名字2
String tName2 = mythread2.getName();
System.out.println(tName0); // output: Thread-0
System.out.println(tName1); // output: Thread-1
System.out.println(tName2); // output: Thread-2
}
}
class Mythread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("分支线程--->" + i);
}
}
}
4.2. 设置线程对象的名字
方法: 通过setName("线程名字")
方法可以获取线程对象的名字。线程对象.setName("线程名字");
代码:
package com.thread;
public class ThreadTest {
// 这里是main方法,这里的代码属于主线程,在主栈中运行
public static void main(String[] args) {
// 创建线程对象
Mythread mythread = new Mythread();
// 设置线程对象名字
mythread.setName("t1");
// 获取线程对象的名字
String tName = mythread.getName();
System.out.println(tName); // output: t1
}
}
class Mythread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("分支线程--->" + i);
}
}
}
4.3. 获取当前线程对象
方法: 通过Thread.currentThread()
方法可以获取线程对象的名字。Thread 变量 = Thread.currentThread();
代码:
package com.thread;
public class ThreadTest {
// 这里是main方法,这里的代码属于主线程,在主栈中运行
public static void main(String[] args) {
// 这个代码出现在main方法中,所以当前线程就是主线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()); // main
}
}
5. 线程的sleep方法
5.1. 用法
static void sleep(long millis)
- 静态方法:
Thread.sleep(1000)
- 参数是毫秒
- 作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有的CPU时间片,让给其他线程使用。
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
// 让当前线程进入休眠,睡眠5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello World!");
}
}
5.2. sleep方法的面试题
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread();
t.setName("t");
t.start();
try {
// 问题:这行代码会让线程t进入休眠状态吗?
t.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello World!");
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
问题: 上面的代码会让线程t进入休眠状态吗?
答: 不会
解释: t.sleep(1000*5);
在执行的时候最终会转换成:Thread.sleep(1000*5);
。记住这段代码的作用是:让当前线程进入休眠,也就是说main方法会进入休眠。因为这段代码出现在main方法中,因此main线程睡眠。
5.3. 终止线程的睡眠
方法: interrupt()
;线程对象.interrupt();
说明: 这种中断睡眠的方式依靠的是java的异常处理机制
代码:
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
// 希望5秒之后,t线程醒来(5秒后主线程手里的活干完了)
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止t线程的睡眠(这种中断睡眠的方式依靠的是java的异常处理机制),run方法中会抛出异常
t.interrupt();
}
}
class MyRunnable implements Runnable {
// 重点:run()方法当中的异常不能throws,只能try catch
// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--->" + "begin");
try {
// 睡眠一年
Thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
System.out.println("停止休眠");
}
System.out.println(Thread.currentThread().getName() + "--->" + "end");
}
}
------------------------------------------------------
Thread-0--->begin
停止休眠
Thread-0--->end
6. 终止线程的执行
6.1. 强行终止
用法: stop()
方法;线程对象.stop();
(已过时,不建议使用)
说明: 这种方法存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死,线程没有保存的数据将会丢失。
代码:
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
// 希望5秒之后,终止线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 强行终止
t.stop(); // 已过时,不建议使用
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
6.2. 合理终止
事先设置布尔类型的变量,通过true或者false进行判断,如果为false就return。
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
// 希望5秒之后,终止线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 合理终止
myRunnable.flag = false;
}
}
class MyRunnable implements Runnable {
boolean flag = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (flag) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
// return之前你可以写一些代码,比如保存内容之类的
return;
}
}
}
}
7. 线程调度
7.1 概述
常见的线程调度模型有哪些
- 抢占式调度模型
哪个线程的优先级比较高,抢到的CPU时间片的概率相对来说高一些或者说是多一些(java采用的就是抢占式调度模型) - 均分式调度模型
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
7.2 常用方法
-
实例方法
-
void setPriority(init newPriority)
:设置线程的优先级 -
int get Priority()
:获取线程优先级
最低优先级1
默认优先级5
最高优先级10package com.thread; public class ThreadTest { public static void main(String[] args) { System.out.println("最高优先级:" + Thread.MAX_PRIORITY); // 最高优先级:10 System.out.println("最低优先级:" + Thread.MIN_PRIORITY); // 最低优先级:1 System.out.println("默认优先级:" + Thread.NORM_PRIORITY); // 默认优先级:5 System.out.println("当前线程优先级:" + Thread.currentThread().getPriority()); // 当前线程优先级:5 Thread.currentThread().setPriority(10); System.out.println("当前线程优先级:" + Thread.currentThread().getPriority()); // 当前线程优先级:10 } }
-
void join()
:合并线程
当前线程t进入阻塞,另一个线程t1执行,直到t1线程结束,当前线程t才可以再次运行
-
-
静态方法
-
static void yield()
:让位方法
暂停当前正在执行的线程对象,并执行其他的线程;yield()方法不是阻塞方法。让当前线程让位,让给其他线程使用;yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。package com.thread; public class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread t = new Thread(myRunnable); t.start(); } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("分支线程1_begin"); for (int i = 0; i < 100; i++) { System.out.println("分支线程1_begin" + i); } MyRunnable1 myRunnable1 = new MyRunnable1(); Thread t1 = new Thread(myRunnable1); t1.start(); try { t1.join(); // t1线程合并到当前线程中,当前线程阻塞,t1线程执行直到结束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("分支线程1_end"); } } class MyRunnable1 implements Runnable { @Override public void run() { System.out.println("分支线程2_begin"); for (int i = 0; i < 100; i++) { System.out.println("分支线程2_begin" + i); } System.out.println("分支线程2_end"); } }
-
8. 线程安全问题
8.1. 概述
这部分内容是重点
以后的开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义吗,线程对象的创建,线程的启动等,都已经实现完毕。这些代码我们都不需要编写。最重要的是,我们编写的程序放到一个多线程的环境下运行,你更需要关心的是这些数据在多线程并发的环境下是否安全。
什么时候数据在多线程并发的环境下会存在安全问题?
- 多线程并发
- 共享数据
- 共享数据有修改的行为
8.2. 线程同步机制
线程排队执行。用排队执行解决线程安全问题,不能并发。这种机制叫做线程同步机制。专业术语叫做线程同步。线程同步就是线程排队,线程排队就会牺牲一部分效率。
同步与异步编程
- 异步编程模型
线程t1和t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等待谁,这种编程模型叫做:异步编程模型。其实就是多线程并发(效率高) - 同步编程模型
线程t1和t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在线程t2执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程的模型(效率低)。线程排队执行。
模拟两个线程对同一个账户取款造成的安全问题
当两个账户在网络延迟下对同一个账户进行取款,会造成账户只扣一次钱,造成财产损失。以下代码虽然两个线程取款了两次,但是最后只扣了5000块钱。
Account
package com.threadSafe;
/*
* 银行账户
* */
public class Account {
// 账号
private String actno;
// 余额
private double balance;
// 无参构造方法
public Account() {
}
// 有参构造方法
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public double getBalance() {
return balance;
}
public void setActno(String actno) {
this.actno = actno;
}
public void setBalance(double balance) {
this.balance = balance;
}
// 取款方法
public void withdraw(double money) {
// 取款之前的余额
double before = this.getBalance();
// 取款之后的余额
double after = before - money;
// 模拟网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新余额
this.setBalance(after);
}
}
AccountThread
package com.threadSafe;
/*
* 两个取款线程
* */
public class AccountThread extends Thread {
// 两个线程必须共享同一个账户
private Account act;
// 通过构造方法传递过来的账户对象
public AccountThread(Account act) {
this.act = act;
}
@Override
public void run() {
// run方法的执行表示取款操作
// 假设账户取款5000
double money = 5000;
// 取款
act.withdraw(money);
System.out.println("线程:" + Thread.currentThread().getName() + "对账户" + act.getActno() + "取款成功,账户余额:" + act.getBalance());
}
}
Test
package com.threadSafe;
/*
* 测试程序
* */
public class Test {
public static void main(String[] args) {
// 创建一个账户对象
Account act = new Account("act-001", 10000);
// 创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
// 设置线程名称
t1.setName("t1");
t2.setName("t2");
// 启动线程
t1.start();
t2.start();
}
}
模拟两个线程对同一个账户取款造成的安全问题——解决办法
加入线程同步机制
语法是:
synchronized() {
// 线程同步代码块
}
synchronized后面的小括号中传的这个”数据“相当关键
这个数据必须是多线程共享的数据,才能达到多线程排队的效果
()中写什么?要看你想让哪些线程同步。假设t1,t2,t3,t4,t5有5个线程,你只希望t1,t2,t3排队,t4和t5不需要排队,那你一定要在()中写一个t1,t2,t3共享的对象,而这个对象对于t4和t5来说是不共享的。
所以只要修改Account中的取款代码即可
Account
// 取款方法
public void withdraw(double money) {
// 以下这几行代码必须是线程排队执行的,不能并发。一个线程执行结束,另一个线程才能开始执行
synchronized (this) {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
8.3. synchronized
java中,任何一个对象都有”一把锁“,这把锁只是一个标记(只是把它叫做锁而已)。100个对象就有100把锁。
上面代码的执行原理:
- 假设t1和t2线程并发,开始执行以上代码的时候,肯定有一个先一个后
- 假设t1先执行了,遇到了synchronized,这个时候自动找”后面共享对象“的对象锁,找到之后,占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的,直到同步代码块结束,这把锁才会释放
- 假设t1已经占有这把锁了,此时t2也遇到了synchronized关键字,也会去占有”后面共享对象“的对象锁,但是这把锁已经被t1占有,t2只能在同步代码块外面等待t1执行结束,直到t1把同步代码块执行结束,t1归还了这把锁。t2才能占有这把锁,然后t2进入同步代码块执行程序。
上面代码中我们传入的是一个this,代表当前对象。
public void withdraw(double money) {
synchronized (this) {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
传入一个字符串可以吗?可以的,字符串存储在字符串常量池,只有一个,是共享的对象
public void withdraw(double money) {
synchronized ("abc") {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
传入一个空指针对象可以吗?不可以
public void withdraw(double money) {
objects o = null;
synchronized (o) {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
下面这种情况可以吗?不可以,因为他们不是共享对象
public void withdraw(double money) {
objects o = new objects;
synchronized (o) {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
哪些变量有安全问题
- 实例变量:在堆中
- 静态变量:在方法区中
- 局部变量:在栈中
以上三大变量中:
- 局部变量永远都不会存在线程安全问题。因为局部变量不共享(一个线程一个栈),局部变量在栈中。
- 实例变量在堆中,堆只有一个。堆是多线程共享的,可能存在安全问题。
- 静态变量在方法区中,方法区只有一个。方法区是多线程共享的,可能存在安全问题。
上面的代码中synchronized都是出现在代码块上,那么实例方法上可以使用synchronized吗? 可以的。
需要注意的是:
- synchronized出现在实例方法上,一定锁的是this,不能是其他,所以这种方式不灵活。
- 此外synchronized出现在实例方法上,表示整个方法都需要同步,可能会无故扩大同步的范围,导致程序的执行效率低,因此这种方式不常用。
// 取款方法
public synchronized void withdraw(double money) {
// 以下这几行代码必须是线程排队执行的,不能并发。一个线程执行结束,另一个线程才能开始执行
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
synchronized的三种写法
-
同步代码块(灵活)
synchronized(线程共享对象) { 同步代码块; }
-
在实例方法上使用synchronized
表示共享对象一定是this,并且同步代码块是整个方法体 -
在静态方法上使用synchronized
表示找类锁。类锁永远只有1把。就算创建了100个对象,那类锁也只有1把。
synchronized面试题1: doOther方法执行的时候需要等待doSome方法的结束吗?
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new MyRunnable(mc);
Thread t2 = new MyRunnable(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyRunnable extends Thread {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("t2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public void doOther() {
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
不需要。因为doOther方法没有synchronized方法。
synchronized面试题2: doOther方法执行的时候需要等待doSome方法的结束吗?
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new MyRunnable(mc);
Thread t2 = new MyRunnable(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyRunnable extends Thread {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("t2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public synchronized void doOther() {
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
需要。因为doOther方法有synchronized方法。
synchronized面试题3: doOther方法执行的时候需要等待doSome方法的结束吗?
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new MyRunnable(mc1);
Thread t2 = new MyRunnable(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyRunnable extends Thread {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("t2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public synchronized void doOther() {
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
不需要。因为不是同一个对象。
synchronized面试题4: doOther方法执行的时候需要等待doSome方法的结束吗?
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new MyRunnable(mc1);
Thread t2 = new MyRunnable(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyRunnable extends Thread {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("t2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized static void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public synchronized static void doOther() {
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
需要。因为synchronized 夹在静态方法上面,表示他是类锁,他的类锁是MyClass,只有一把,因此需要等待。
8.4. 如何解决线程安全问题
解决线程安全问题,不要一上来就使用synchronized 。因为synchronized 会导致程序的执行效率降低,用户体验不好。系统的吞吐量降低,用户体验差。
一共有三种方案:
- 尽量使用局部变量代替“实例变量”和“静态变量”
- 如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。因为1个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了
- 如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized 了
9. 死锁
死锁的代码怎么写?了解死锁的写法,防止以后遇到这种情况,因为死锁很难调试,他不会报错,也不会成功运行。
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
MyRunnable1 t1 = new MyRunnable1(o1, o2);
MyRunnable2 t2 = new MyRunnable2(o1, o2);
t1.start();
t2.start();
}
}
class MyRunnable1 extends Thread {
Object o1;
Object o2;
public MyRunnable1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(111);
}
}
}
}
class MyRunnable2 extends Thread {
Object o1;
Object o2;
public MyRunnable2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(222);
}
}
}
}
结论:synchronized 在开发中最好不要嵌套使用,很有可能引发死锁
10. 守护线程
java语言中线程分为两大类:
- 用户线程
- 守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
实现一个简单的守护线程:
下面的代码模拟了用户线程和备份数据的例子,当十个循环之后,用户线程结束了,但是备份数据是一个死循环,永远也不会结束,那么如何使备份数据线程随着用户线程结束而结束呢?也就是说使备份数据线程作为一个守护线程。
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new MyRunnable1();
t1.start();
// 主线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "用户线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable1 extends Thread {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "--->" + "备份数据");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
很简单,在t.start();
前加一句t.setDaemon(true);
package com.thread;
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new MyRunnable1();
// 使其成为守护线程
t1.setDaemon(true);
t1.start();
// 主线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "用户线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable1 extends Thread {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "--->" + "备份数据");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
11. 定时器
11.1. 概述
定时器的作用:间隔特定的时间,执行特定的程序
11.2. 实现一个简单的定时器
package com.thread;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*
* 使用定时器指定定时任务
* */
public class ThreadTest {
public static void main(String[] args) throws ParseException {
// 创建定时器对象
Timer timer = new Timer();
// Timer timer = new Timer(true); // 守护线程的方式
// 指定定时任务
// timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2021-04-10 21:02:00");
timer.schedule(new LogTimerTask(), firstTime, 5000);
}
}
// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {
@Override
public void run() {
// 编写你要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ":成功完成了一次数据备份!");
}
}
12. wait和notify
12.1. 概述
-
wait和notify不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是Object类中自带的。
-
wait()方法的作用?
Object o = new Object(); o.wait();
表示:让正在o对象上活动的t线程进入无限期等待状态,并释放t线程之前占有的o对象的锁,直到被唤醒为止。
-
notify()方法的作用?
Object o = new Object(); o.notify();
表示:唤醒正在o对象上等待的线程。
还有一个notifyAll()
方法,用于唤醒o对象上处于等待的所有线程。
12.2. 生产者和消费者模式
- 使用
wait
和notify
方法实现“生产者和消费者模式“ - 什么是“生产者和消费者模式”?
- 生产线程负责生产,消费线程负责消费
- 生产线程和消费线程要达到均衡
- 这是一种特殊的业务需求,需要使用
wait
和notify
方法
wait
和notify
方法不是线程的方法,是普通java对象都有的方法wait
和notify
方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库,有线程安全问题。- 下面我们模拟这样的一个需求
- 仓库采用List集合
- List集合中假设只能存储一个元素,1个元素表示仓库满了
- 如果List集合中元素个数是0,表示仓库空了
- 保证List集合中永远都是最多存储1个元素
- 必须做到:生产1个消费1个
package com.thread;
import java.util.ArrayList;
import java.util.List;
public class ThreadTest {
public static void main(String[] args) {
// 创建1个仓库对象,共享的
List list = new ArrayList();
// 创建两个线程对象
// 生产者线程
Thread t1 = new Thread(new Producer(list));
// 消费者线程
Thread t2 = new Thread(new Consumer(list));
// 设置名称
t1.setName("生产者线程");
t2.setName("消费者线程");
// 启动
t1.start();
t2.start();
}
}
// 生产线程
class Producer implements Runnable {
// 定义一个仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直生产(模拟死循环来一直生产)
while (true) {
// 给仓库对象加锁
synchronized (list) {
if (list.size() > 0) { // 大于0说明仓库中已经有一个元素了
// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到这里,说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 再唤醒消费者进行消费
list.notify();
}
}
}
}
// 消费线程
class Consumer implements Runnable {
// 定义一个仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直消费
while (true) {
synchronized (list) {
if (list.size() == 0) {
try {
// 仓库已经空了
// 消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到这里,说明仓库满了,需要消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒生产者生产
list.notify();
}
}
}
}
13. 线程池(参考)
**线程池:**三大方法、七大参数、4种拒绝策略
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
13.1 三大方法
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
有线程池之后,就要使用线程池来创建线程。
三种方法创建线程池:
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
Executors.newSingleThreadExecutor(); // 单个线程
Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱
}
}
- Executors.newSingleThreadExecutor();
- Executors.newFixedThreadPool(5);
- Executors.newCachedThreadPool();
13.1.1 第一种方法
单个线程:Executors.newSingleThreadExecutor();
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
public class ThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();// 单个线程
try {
for (int i = 0;i < 10;i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown(); // 线程池关闭
}
}
}
输出结果,可以看到只有一个线程一直在执行。
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
13.2.2 第二种方法
固定大小线程池:Executors.newFixedThreadPool(5);
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
输出结果,可以看到最高有五个线程在执行。
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-5 ok
pool-1-thread-4 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
13.3.3 第三种方法
可伸缩的:Executors.newCachedThreadPool();
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
输出结果,可以看出for
循环10次,最高可以有10个线程产生
pool-1-thread-3 ok
pool-1-thread-6 ok
pool-1-thread-5 ok
pool-1-thread-4 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-9 ok
pool-1-thread-8 ok
pool-1-thread-7 ok
pool-1-thread-10 ok
13.2 七大参数
首先分析一波源码,先看看三大方法的源码。
// (1)newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// (2)newFixedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// (3)newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
从上面源码可以发现他们三大方法都调用了ThreadPoolExecutor
方法,那我们查看ThreadPoolExecutor
方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
ThreadPoolExecutor
中使用了this
方法,我们再看下this
方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
最终,我们可以看见七大参数就在这里面,反过来推,就知道三大方法是怎么来的了。
- int corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量。
- int maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- long keepAliveTime:当线程池中的线程数量大于
corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁; - TimeUnit unit:参数的时间单位。
- BlockingQueue workQueue:阻塞队列。当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
- ThreadFactory threadFactory:executor 创建新线程的时候会用到。
- RejectedExecutionHandler handler:拒绝策略
13.3 四种拒绝策略
- new ThreadPoolExecutor.AbortPolicy():如果满了,此时还有进来的,就不处理,并抛出
RejectedExecutionException
来拒绝新任务的处理。 - new ThreadPoolExecutor.CallerRunsPolicy():调用执行自己的线程运行任务,也就是直接在调用
execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 - new ThreadPoolExecutor.DiscardPolicy():队里满了,丢掉任务,不会抛出异常
- new ThreadPoolExecutor.DiscardOldestPolicy():队列满了,尝试去和最早的竞争,不会抛出异常
13.4 自定义线程池
一开始我们知道《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而分析源码又可以得出我们的三大方法其实最终是调用的ThreadPoolExecutor
方法,因此我们使用ThreadPoolExecutor
来自定义创建线程池。
import java.util.concurrent.*;
public class ThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
// 最大承载 = maximumPoolSize + workQueue
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}