文章目录
多线程学习总结
多线程概述
- 什么是进程?什么是线程?
一个进程是一个应用程序,或者说是一个软件
线程是进程中的执行场景/执行单元
一个进程可以启动多个线程
- 举个例子
在dos窗口中当输入java HelloWorld 回车之后 会先启动JVM,而JVM就是一个进程。
JVM会再启动一个主线程调用main方法,同时还会启动一个垃圾回收线程来负责看护回收垃圾
此时最起码再进程当中有两个线程是并发的,一个是执行main方法的主线程 一个是执行垃圾回收的线程
- 进程和线程之间的关系
进程可以看作现实生活当中的公司
线程可以看作是公司当中的某个员工
比如:
公司:京东就可以看作是一个进程
员工:强东 就可以看作是一个线程
员工:妹妹 也可以看作是一个线程
- 注意
1、假如有两个进程A和B进程A和进程B他们之间的内存是不能够共享的,他们是独立的
比如:QQ音乐和网易云音乐是两个独立的进程他们之间内存不能够共享,是相互独立的
2、假如有两个线程A、B
在java当中,线程A、B之间共享堆内存和方法区,但是他们两个的栈内存是独立的,一个线程一个栈,每个线程之间执行互不干扰,各自执行各自的,这个就是多线程
3、java中存在多线程机制的原因:提高程序的处理效率
- 注意一个问题
采用了多线程机制之后,main方法执行完毕之后程序不一定会执行结束,因为main方法执行完毕之后,只是主线程结束了,只是主栈空了其他的栈可能还在弹栈、压栈
- 图解线程和线程之间的关系
多线程并发的理解
- 分析一个问题对于单核的CPU能够真正的做到多线程并发吗?
答案:不能!但是单核的CPU可以给人一种多线程并发的错觉
实际上对于单核的CPU在某一个时间节点上,只能处理一件事情,但是由于CPU处理的速度极快,线程和线程之间切换频繁,给人的感觉是多个线程同时执行
比如:以前老的电影院播放电影采用胶卷其实就是一张张的照片进行切换,只不过速度很快让人感觉到是连续的
但是对于多核CPU来说,比如4核CPU,可以真正做到4个线程并发
分析程序当中存在几个线程
分析一下下面的代码中有几个线程
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:20
**/
public class ThredTest1 {
public static void main(String[] args) {
m1();
}
private static void m1()
{
m2();
}
private static void m2()
{
m3();
}
private static void m3()
{
}
}
虽然在这份代码当中main()方法调用了m1方法,m1方法调用了m2方法,m2方法调用了m3方法,但是这些方法全部都是在main方法的主栈当中执行的,只有一个方法栈,所以在这份代码当中只有一个线程。
- ThredTest1类中方法执行分析图
实现线程的第一种方式
- 编写一个类,让这个类直接继承java.lang.Thread类重写run方法
- 怎么创建线程对象? 直接new我们的分支线程对象即可
- 怎么启动线程?调用start方法启动线程,调用start方法会启动一个分支栈会在JVM当中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了
- 调用start方法只是开辟了一个新的栈空间,只要新的栈空间一开辟,start方法就结束了。分支线程也就启动成功了
- 当线程启动成功之后就会自动调用run方法,并且run方法在新的栈空间的底部(压栈)。
- run方法在分支栈的底部,main方法在主栈的底部,main方法的run方法是平级的
- 如果用我们的线程对象直接调用run方法:myThread.run();会怎么样?=====>不会启动多线程,不会分配新的栈空间(这种方式其实就是一个单线程)
- **注意一个亘古不变的道理:**方法体中的代码永远都是自上而下的顺序逐行依次执行
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:32
**/
public class ThredTest2 {
public static void main(String[] args) {
//这是main方法,这段代码属于主线程,运行在主栈当中
//现在新建一个线程对象
MyThread myThread = new MyThread();
//调用start方法启动线程
myThread.start();
//这下面的代码还是运行在主线程当中
for (int i=0;i<999;i++)
{
System.out.println("这段代码还是运行在主线程当中");
}
}
}
class MyThread extends Thread
{
@Override
public void run() {
//编写程序,输出这段程序运行在分支栈中
for (int i = 0; i<999;i++)
{
System.out.println("这段程序运行在分支栈中");
}
}
}
strat和run的区别
- 调用start方法才是真正的多线程并发,在调用start方法的时候,会在mian方法的这个主栈的外边新开辟一个栈,在这个栈空间开辟成功之后,这个方法立即失效然后JVM会默认自行调用run方法,将这个run这个方法压在新开辟的栈底当中。
- 调用run方法本质还是个单线程,他还是在main方法中执行的并没有开辟新的栈空间
- 有这样一份代码
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:32
**/
public class ThredTest2 {
public static void main(String[] args) {
//这是main方法,这段代码属于主线程,运行在主栈当中
//现在新建一个线程对象
MyThread myThread = new MyThread();
//调用start方法启动线程
myThread.start();
//调用run方法,对比一下调用start方法的区别
//这下面的代码还是运行在主线程当中
for (int i=0;i<100;i++)
{
System.out.println("主方法栈=====>"+i);
}
}
}
class MyThread extends Thread
{
@Override
public void run() {
//编写程序,输出这段程序运行在分支栈中
for (int i = 0; i<100;i++)
{
System.out.println("新开辟的栈=====>"+i);
}
}
}
从这个运行结果可以看出这是调用了start真正的实现了多线程,他的新开辟的栈和主栈时交替运行的,充分说明了这是两个线程并发的下面看看他的运行原理图
- 另外一份代码
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:32
**/
public class ThredTest2 {
public static void main(String[] args) {
//这是main方法,这段代码属于主线程,运行在主栈当中
//现在新建一个线程对象
MyThread myThread = new MyThread();
//调用start方法启动线程
//myThread.start();
//调用run方法,对比一下调用start方法的区别
myThread.run();
//这下面的代码还是运行在主线程当中
for (int i=0;i<100;i++)
{
System.out.println("主方法栈=====>"+i);
}
}
}
class MyThread extends Thread
{
@Override
public void run() {
//编写程序,输出这段程序运行在分支栈中
for (int i = 0; i<100;i++)
{
System.out.println("新开辟的栈=====>"+i);
}
}
}
从这个调用了run方法执行的结果分析:分明显并没有开启多线程,因为他是从上向下依次调用了run方法,执行完run方法中中的输出才开始执行mian方法中的输出,下面看看他的运行结构图
- 总结一下调用run方法只是new了一个继承了线程的类,应没有启动多线程,它的本质还是一个单线程
实现多线程的第二种方式
-
采用接口的方式实现多线程
-
编写一个类实现java.lang.Runnable接口重写run方法
-
需要注意的是自定义的这个实现了Runnable接口的方法并不是一个多线程对象,他需要封装到Thread类中才能获得这个多线程对象
-
然后拿这个多线程对象,调用start方法,就能成功开启多线程了
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 22:54
**/
public class ThredTest3 {
public static void main(String[] args) {
//new 一个可运行的对象
MyRunnable myRunnable = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread thread = new Thread(myRunnable);
//调用start方法启动多线程
thread.start();
for (int i = 0;i<99;i++)
{
System.out.println("主方法栈===>"+i);
}
}
}
//创建一个类实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<99;i++)
{
System.out.println("分支栈===>"+i);
}
}
}
- 执行结果
明显看出分支栈和主方法栈并发执行,这就是开启多线程的第二种方法
- 注意以上两种实现多线程的方式我们推荐实现Runnable接口的方式,因为用实现接口的方式,这个类还可以继承别的类,更加灵活然而采用继承的方式就不能再实现别的类了
采用匿名内部类的方式
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 23:18
**/
public class ThredTest4 {
public static void main(String[] args) {
//采用匿名内部类的方式来开启多线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<99;i++)
{
System.out.println("分支栈===>"+i);
}
}
});
//调用start方法开启多线程
thread.start();
for (int i=0;i<99;i++)
{
System.out.println("主方法栈===>"+i);
}
}
}
- 成功开启多线程
线程的生命周期
从上面一系列代码可以总结出输出结果的一些特点:
- 分支栈和主方法栈时有先有后
- 有多有少
分析这个问题就需要了解一下线程的生命周期
包含六个周期:
- 新建状态:刚new出来线程对象
- 就绪状态:新建状态调用了start方法后就会进入到就绪状态,就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺cpu时间片的权利(cpu时间片就是执行权力)当一个线程抢到时间片之后,就可以开始执行run方法,run方法执行成功之后标志着线程进入到了运行状态。
- 运行状态:run方法开始执行就标志着线程进入到运行状态,当之前占有的cpu时间片执行完之后,会重新回到就绪状态,继续抢夺cpu时间片,当再次抢夺到cpu时间片之后,就会再次进入到运行状态,接着执行run方法,继续往下执行
- 阻塞状态:当一个线程遇到阻塞事件之后,例如接受用户从键盘输入,或者sleep方法等,此时线程会进入到阻塞状态,阻塞状态的线程会放弃之前占用的cpu时间片,当阻塞解除的时候,需要这个线程继续回到就绪状态继续抢夺cpu时间片
- 死亡状态:运行状态时当run方法执行结束就会进入死亡状态,整个线程也就到此结束了
获取当前线程
- 获取线程名字的方法通过new的线程对象调用myThreTest1.getName()方法
- 如果自己不设置线程的名字的话,线程的名字默认是Thread-x,第一个是Thread-0,第二个是Thread-1,第三个是Thread-2 …
- 设置线程名字的方法通过new的线程对象调用setName()方法
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 0:30
**/
public class ThresTest5 {
public static void main(String[] args) {
myThreTest myThreTest1 = new myThreTest();
String name1 = myThreTest1.getName();
myThreTest myThreTest2=new myThreTest();
String name2 = myThreTest2.getName();
System.out.println(name1);
System.out.println(name2);
}
}
class myThreTest extends Thread{
@Override
public void run() {
for (int i=0;i<99;i++)
{
System.out.println();
}
}
}
- 默认的线程名称
- 通过调用setName方法来设置线程名称
myThreTest myThreTest1 = new myThreTest();
myThreTest1.setName("线程一");
String name1 = myThreTest1.getName();
myThreTest myThreTest2=new myThreTest();
myThreTest2.setName("线程二");
String name2 = myThreTest2.getName();
System.out.println(name1);
System.out.println(name2);
- 获取当前线程,调用thread.currentThread方法
- 在哪个线程中调用thread.currentThread就获取到哪个线程
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 0:30
**/
public class ThresTest5 {
public static void main(String[] args) {
Thread thread = new Thread();
Thread cur = thread.currentThread();
System.out.println("当前线程的名称:"+cur.getName());
myThreTest myThreTest1 = new myThreTest();
myThreTest1.setName("线程一");
String name1 = myThreTest1.getName();
myThreTest myThreTest2=new myThreTest();
myThreTest2.setName("线程二");
String name2 = myThreTest2.getName();
/* System.out.println(name1);
System.out.println(name2);*/
myThreTest1.start();
myThreTest2.start();
}
}
class myThreTest extends Thread{
Thread thread = new Thread();
@Override
public void run() {
//在分支线程当中调用currentThread就获取到分支线程
System.out.println("获取到分支线程名称:"+thread.currentThread().getName());
/* for (int i=0;i<99;i++)
{
System.out.println();
}*/
}
}
- main方法对应的线程如果不修改名称,获取到的线程对象名字就叫做main
- 总结就是一句话:thread.currentThread在哪个线程中调用返回的就是哪个线程
sleep方法
-
参数是毫秒
-
是一个静态方法
-
sleep方法在哪个进程当中调用就在让那个进程进行休眠
-
实例一个方法,每隔2秒获取一下当前线程的名称获取10次
package com.zb.test;/** * @Author 啵儿 * @Email 1142172229@qq.com * @date 2021/10/26 1:13 **/public class ThredTest6 { public static void main(String[] args) { myRunnable run = new myRunnable(); Thread thread = new Thread(run); thread.start(); }}class myRunnable implements Runnable{ @Override public void run() { for (int i=0;i<10;i++) { //每隔5秒输出分支线程的名称 try { Thread.sleep(1000*2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("分支线程的名称"+Thread.currentThread().getName()); } }}
- 关于sleep方法的一道面试题
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 1:25
**/
public class ThredTest7 {
public static void main(String[] args) {
Thread t = new myThred();
t.setName("分支线程1");
t.start();
try {
t.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class myThred extends Thread{
@Override
public void run() {
System.out.println("====>分支线程执行");
}
}
-
在这份代码中myThred这个分支线程会不会睡眠
-
答案:不会的!因为sleep是一个静态方法,他在哪个线程中调用就让哪个线程睡眠,更哪个对象去调用没有关系,主要看的是他在哪个线程中被调用!
-
终止一个线程的睡眠(注意是终止线程的睡眠,不是终止整个线程),调用thread.interrupt()方法
-
测试用例,现在有一个一睡睡一年的线程,现在想办法在主线程当中终止它
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 8:34
**/
public class ThredTest8 {
public static void main(String[] args) {
Thread thread = new Thread(new myRunnable1());
thread.setName("分支线程");
//启动分支线程
thread.start();
//设计一下希望5秒之后终止分支线程睡眠
try {
thread.sleep(2*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止分支线程睡眠,这种终止方式,依靠java的异常处理机制
thread.interrupt();
for (int i = 0;i<10;i++)
{
System.out.println("主栈===>"+i);
}
}
}
class myRunnable1 implements Runnable{
@Override
public void run() {
Thread thread = new Thread();
//run方法当中的异常不能throws只能try catch
//因为run方法在父类中没有抛出任何异常,子类不能比父类抛出更宽泛的异常
try {
//睡眠一年
thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0;i<10;i++)
{
System.out.println("分支栈==>"+i);
}
}
}
- 执行结果
- 强行终止一个线程的执行
thread.stop();
-
注意这种方式存在很大的缺陷:容易丢失数据,因为这种方式是直接将线程杀死了,线程没有保存的数据,上面的sleep.interrupted是将睡眠的线程醒过来,打印异常继续执行,而这种stop是直接杀死异常(所以这种方式不建议使用)
-
怎样合理的终止一个线程的执行(通过打标记的方式,这样可以做到自主控制,避免造成数据丢失,一种常用的方式)
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 9:02
**/
public class ThredTest9 {
public static void main(String[] args) {
myThred1 myThred1 = new myThred1();
myThred1.start();
//模拟5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
//你想要什么时候终止线程,就将标记修改即可
myThred1.run=false;
}
}
class myThred1 extends Thread{
//打一个布尔标记,用作在主线程中修改来控制分线程的睡眠状态
boolean run = true;
@Override
public void run() {
for (int i=0;i<10;i++)
{
if(run)
{
System.out.println("分支栈===>"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
//return 就结束了,如果在结束之前需要保存什么数据,在这里保存即可
return;
}
}
}
}
线程调度
- 线程调度的概述
常见的线程调度方式:
抢占式线程调度:哪个线程的优先级高,抢夺的cpu时间片概率就高一些,java采用的就是抢占式调度模型
均分式调度模型:平局被分配cpu时间片,每个线程占用cpu时间片时间长度一样,平均分配,时间长度相等
- 线程调度的方法
实例方法:
void setPriority(int newProiority) 设置线程优先级
int getPriority()获取线程优先级
最低优先级:1
线程默认优先级:5
最高优先级:10
优先级比较高的cpu时间片可能会多一些,(但也不完全是,大概率是多的)
静态方法:
static void yield() 让位方法
暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法,而是让当前线程让位给其他线程执行
yield()方法的执行会让当前线程从"运行状态"回到"就绪状态"
注意:在回到就绪状态之后,有可能还会再次抢到
实例方法:
void join() 合并线程
- 调用join方法实现两个线程合并
- 调用join方法会使当前线程堵塞,直到myThred3线程执行结束,当前线程才能继续
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 15:48
**/
public class ThredTest10 extends Thread {
@Override
public void run() {
myThred3 myThred3 = new myThred3();
try {
//调用join方法实现两个线程合并
//调用join方法会使当前线程堵塞,直到myThred3线程执行结束,当前线程才能继续
myThred3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
}
class myThred3 extends Thread{
@Override
public void run() {
super.run();
}
}
-
手动设置线程优先级
通过t.setPriority()设置线程优先级
package com.zb.test; /** * @Author 啵儿 * @Email 1142172229@qq.com * @date 2021/10/26 16:09 **/ public class ThredTest11 { public static void main(String[] args) { //设置主线程优先级 Thread t2 = new Thread(); Thread.currentThread().setPriority(1); myThred4 myThred4 = new myThred4(); myThred4.setPriority(10); myThred4.setName("t"); myThred4.start(); } } class myThred4 extends Thread{ @Override public void run() { for (int i = 0;i<100;i++) { Thread t = new Thread(); System.out.println(t.getName()+"===>"+i); } } }
多线程的安全的问题(重点)
- 以后我们开发的项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动,都已经实现完了,这些代码我们都不需要编写
- 然而最重要的是:你要知道,你编写的程序需要放在一个多线程的环境下执行,更要关注的是这些数据在多线程的环境下是否安全
- 什么时候数据在多线程环境下会存在线程安全的问题呢?
三个条件:
- 条件一:多线程并发
- 条件二:有共享数据
- 条件三:共享数据有修改行为
满足上述三个条件之后,就会存在线程安全问题。
多线程安全问题的解决
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,解决这个问题的方法就是想办法让线程排队执行(不能并发),用排队执行解决线程安全问题,这种机制被称为:线程安全机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行
怎么解决线程安全问题?
使用线程同步机制。
线程同步就是让线程排队执行,线程排队会牺牲一部分的效率,没办法,数据安全因该放在第一位,只有数据安全了才能谈效率,数据不安全,就没有效率的事。
- 通过银行取款实例模拟线程安全问题
咱就是说,现在有两个人同时从一个银行账户当中取钱,如果不采用线程同步机制会出现什么问题呢?
- 定义一个银行账户类
- 注意在账户类当中,在进行余额更新的时候,调用了sleep方法,模拟一下网络延迟
package com.zb.pojo;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 17:35
**/
public class Bank {
private String id;//账户id
private int ye;//账户余额
public Bank() {
}
public Bank(String id, int ye) {
this.id = id;
this.ye = ye;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getYe() {
return ye;
}
public void setYe(int ye) {
this.ye = ye;
}
public void qk(int many)
{
//取款之前的余额
int beform = this.ye;
//取款之后的余额
int afterm=beform-many;
//假设在更新余额之前模拟一下网络延迟
Thread t = new Thread();
try {
t.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setYe(afterm);
}
}
- 定义一个线程类,用于执行取款方法
package com.zb.ThredBank;
import com.zb.pojo.Bank;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 17:43
**/
public class BankThred extends Thread {
//两个线程必须共享同一个账户
//通过构造方法传过来一个实例化对象
private Bank bank;
public BankThred (Bank bank)
{
this.bank=bank;
}
@Override
public void run() {
Thread t =new Thread();
//run方法表示取款操作
//假设取款5000
int many = 5000;
bank.qk(many);
//接下来输出取款信息
System.out.println(t.currentThread().getName()+"在账户"+t.getName()+"一共取处金额"+many+"元,账户剩余金额"+bank.getYe());
}
}
- 测试类,new两个线程对象,启动多线程,模拟两个取款人同时对银行账户进行取款操作
package com.zb.ThredBank;
import com.zb.pojo.Bank;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 17:48
**/
public class BankTest {
public static void main(String[] args) {
//创建账户对象
Bank bank = new Bank("zb001",10000);
//创建两个线程
Thread t1 = new BankThred(bank);
Thread t2 = new BankThred(bank);
//设置线程名字
t1.setName("取款人1");
t2.setName("取款人2");
//启动线程进行取款
t1.start();
t2.start();
}
}
- 执行结果
- 发现多线程并发出现了问题
多线程两种模型
- 1、异步编程模型
线程t1和线程t2,各自执行各自的,t1不管t2,t2,不管t1,谁也不需要等谁,这种编程模型叫做异步编程模型,其实就是多线程并发(效率较高)
- 2、同步编程模型
线程t1和线程t2,在线程t1执行的时候,必须等待线程t2执行结束,或者说在线程t2执行的时候必须等待线程t1执行结束,两个线程之间发生了等待关系,这就是同步编程模型,效率较低。线程需要排队执行
-
3、同步就是排队,异步就是并发
-
下来采用线程同步机制对上面的代码进行修复
其实采用线程同步的方式很简单,只需要在上面代码的基础上加入synchronized
将两个线程搜需要执行的方法,用synchronized处理一下
synchronized (this)
{
//取款之前的余额
int beform = this.ye;
//取款之后的余额
int afterm=beform-many;
//假设在更新余额之前模拟一下网络延迟
Thread t = new Thread();
try {
t.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setYe(afterm);
}
- 其实最重要也是最难理解的是synchronized()这个括号当中应该填什么?
- 记住一个原则synchronized()这个括号当中填写的是被共享的对象,假设有t1,t2,t3,t4,t5一共五个线程,只需要t1、t2和t3执行同步机制,t4,t5不需要执行同步机制,那么括号当中填写的就是t1,t2,t3共享的对象,而这个对象对t4,t5来说不能是共享的。
对synchronized的理解
- 首先要明确在java语言中,任何一个对象都有"一把锁",其实这把锁就是标记。(只是把它叫做锁),100个对象就有一百把锁,一个对象一把锁
- 再来分析一下这份代码
synchronized (this)
{
//取款之前的余额
int beform = this.ye;
//取款之后的余额
int afterm=beform-many;
//假设在更新余额之前模拟一下网络延迟
Thread t = new Thread();
try {
t.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setYe(afterm);
}
-
在两个取款人同时取款的时候,就相当于两个线程进行并发操作,开始执行代码的时候,肯定有一个先一个后
-
假设取款人1先执行了,遇到synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码当中的程序,在程序执行过程中一直都是占有这把锁的,知道同步代码块结束,这把锁才会释放。
-
假设取款人1已经占有这把锁,此时取款人2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果发现这把锁已经被取款人1占有了,取款人2只能在同步代码块外面等待取款人1的结束,直到取款人1把同步代码块执行结束,取款人1就会归还这把锁,此时取款人终于等到了这把锁,然后取款人2占有这把锁之后,进入同步代码块执行程序。
-
咱就是说这样的过程就是达到了线程排队执行的
-
这里需要注意的是synchronized()括号中这个共享对象一定要选好,这个共享对象一定是你需要排队去执行这些线程对象所共享的。
-
之前线程的生命周期图可以更新一下,但是这个锁池不属于线程的生命周期,进入锁池可以理解成为一种阻塞状态。
三大变量那些可能存在线程安全问题
- 首先要知道java的三大变量
实例变量:在堆中
静态变量:在方法区中
局部变量:在栈中
以上三大变量中:局部变量绝对不可能存在线程安全问题,因为局部变量不是共享的,一个线程一个栈,局部变量在栈中,所以局部变量永远都不会被共享,所以也不会存在线程安全问题
实例变量在堆中:而堆只有一个
静态变量在方法区中:方法区也只有一个
所以:堆和方法区搜是多线程共享的,所以可能存在线程安全问题
在实例方法上添加synchronized
public synchronized void qk(int many)
{
//取款之前的余额
int beform = this.ye;
//取款之后的余额
int afterm=beform-many;
//假设在更新余额之前模拟一下网络延迟
Thread t = new Thread();
try {
t.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setYe(afterm);
}
}
- synchronized出现在实例方法上一定锁的是this,不能是其它对象了,这样不太灵活
- 还有另外一个缺点就是:synchronized出现在实例方法上表示整个方法体都需要线程同步,导致效率降低
- 它只有一个优点就是 代码简单
总结synchrozined的三种写法
- 第一种:同步代码块(灵活)
synchronized(线程共享对象){
同步代码块
}
-
第二种:在实例方法上使用synchronized表示共享对象一定是this,并且同步代码块是整个方法体
-
但三种:在静态方法上使用synchronized,表示类锁
-
类锁永远只有一把
-
对象锁:一个对象一把锁
几到面试题理解synchronized
- 第一道面试题:下面这段代码中doother方法的执行,需不需要等待dosome的执行
package com.zb.Thredexam;
import com.zb.test.ThredTest1;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 22:26
**/
public class exam1 {
public static void main(String[] args) {
myclasss mc = new myclasss();
Thread t1 = new myThred5(mc);
Thread t2 = new myThred5(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class myThred5 extends Thread{
private myclasss mc;
public myThred5(myclasss mc)
{
this.mc=mc;
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("t1"))
{
mc.dosome();
}
else if (Thread.currentThread().getName().equals("t2"))
{
mc.doother();
}
}
}
class myclasss{
public synchronized void dosome()
{
System.out.println("doSome began");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doother()
{
System.out.println("doOther began");
System.out.println("dpOther over");
}
}
- 答案是不需要,分析上面这段代码,dosome方法被synchronized关键字修饰,执行dosome方法需要锁,而doother方法没有被synchronized关键字修饰,所以t2去执行doother方法是不需要锁的,所以咱就是说总是你dosome方法执行把锁拿走了,我doother方法照样还是能够执行,因为我的执行是不需要锁的。
- 面试题二:在上面那份代码中对doother方法也加synchronized关键字修饰,其余和面试代码一保持一致
package com.zb.Thredexam;
import com.zb.test.ThredTest1;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 22:26
**/
public class exam1 {
public static void main(String[] args) {
myclasss mc = new myclasss();
Thread t1 = new myThred5(mc);
Thread t2 = new myThred5(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class myThred5 extends Thread{
private myclasss mc;
public myThred5(myclasss mc)
{
this.mc=mc;
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("t1"))
{
mc.dosome();
}
else if (Thread.currentThread().getName().equals("t2"))
{
mc.doother();
}
}
}
class myclasss{
public synchronized void dosome()
{
System.out.println("doSome began");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doother()
{
System.out.println("doOther began");
System.out.println("dpOther over");
}
}
- 现在的话执行doother方法就需要等人家dosome方法执行完毕才能执行doother方法,因为我们直到t1,t2两个线程共享mc一个对象,一个对象一把锁,一百个对象一百把锁,当t1线程执行dosome方法时,拿走了锁,t2线程调用doother方法需要等t1线程执行完dosome方法后,归还这把锁才能够让t2线程拿到这把锁去执行doother方法。
- 面试题三:问题还是之前的问题,程序大致也还是之前的程序,只不过在线面这份代码中我new了两个对象,其余和面试代码二保持一致
package com.zb.Thredexam;
import com.zb.test.ThredTest1;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 22:26
**/
public class exam1 {
public static void main(String[] args) {
myclasss mc1 = new myclasss();
myclasss mc2 = new myclasss();
Thread t1 = new myThred5(mc1);
Thread t2 = new myThred5(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class myThred5 extends Thread{
private myclasss mc;
public myThred5(myclasss mc)
{
this.mc=mc;
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("t1"))
{
mc.dosome();
}
else if (Thread.currentThread().getName().equals("t2"))
{
mc.doother();
}
}
}
class myclasss{
public synchronized void dosome()
{
System.out.println("doSome began");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doother()
{
System.out.println("doOther began");
System.out.println("dpOther over");
}
}
- 这分代码中,doother方法的执行需不需要等待dosome方法的执行完毕呢?=====>答案是不会的,因为在这分代码中new了两个对象分别传给线程1和线程2,他俩根本就不会共享同一个对象,而且两个对象就有两把锁,线程1和线程2的执行是互不干扰的。
- 面试题四:在面试题3代码上将俩个方法用static关键字修饰
package com.zb.Thredexam;
import com.zb.test.ThredTest1;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 22:26
**/
public class exam1 {
public static void main(String[] args) {
myclasss mc1 = new myclasss();
myclasss mc2 = new myclasss();
Thread t1 = new myThred5(mc1);
Thread t2 = new myThred5(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class myThred5 extends Thread{
private myclasss mc;
public myThred5(myclasss mc)
{
this.mc=mc;
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("t1"))
{
mc.dosome();
}
else if (Thread.currentThread().getName().equals("t2"))
{
mc.doother();
}
}
}
class myclasss{
public synchronized static void dosome()
{
System.out.println("doSome began");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized static void doother()
{
System.out.println("doOther began");
System.out.println("dpOther over");
}
}
- 在这份代码当中就需要等待了,因为static修饰的方法是静态方法,syncronized修饰静态方法,表示类锁,类锁永远只有一把,所以不管你new了几个对象,你的类锁只有一把,所以需要等待。
死锁
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/27 0:22
**/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 =new Object();
Thread t1 = new myThred6(o1,o2);
Thread t2 = new myThred7(o1,o2);
t1.start();
t2.start();
}
}
class myThred6 extends Thread {
Object o1;
Object o2;
public myThred6(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) {
}
}
}
}
class myThred7 extends Thread {
Object o1;
Object o2;
public myThred7(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2)
{
synchronized (o1)
{
}
}
}
}
开发中应该怎么解决线程安全问题
- 在以后的开发当中并不是一上来就选择synchronized方式解决线程安全问题,因为synchronized关键字会让程序的执行效率变低,用户体验不好,系统吞吐量降低,只有在不得义的情况下才会采用synchronized方式,也就是线程同步机制
- 解决线程安全问题是有以下几种方式的:
- 第一种方案:尽量使用局部变量代替"实例变量"和"静态变量"。因为局部变量不存在线程安全问题
- 第二种方案:如果必须是实例变量,那么可以考虑多创建几个对象,100个线程对应100个对象,这样实例变量就不会共享了,也就不会存在线程安全问题
- 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized,采用线程同步机制
守护线程
- 守护线程概述
java语言当中线程分为两大类:
一类是:用户线程
一类是:守护线程:具有代表性的守护线程就是垃圾回收线程
- 守护线程的特点
一般守护线程都是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主方法main是一个用户线程
守护线程用在什么地方呢?
现在有一个定时备份系统,在每天0:00的时候系统自动备份数据,这个时候需要用到定时器,我们可以将这个定时器设置为守护线程,一直在那里看着,每当0:00的时候就备份一次,所有用户的线程结束了,守护线程就自动退出,没有必要再进行数据备份了。
- 实现守护线程:在线程启动之前调用setDaemo()方法,将线程设置为守护线程
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/27 10:51
**/
public class ThredTest12 {
public static void main(String[] args) {
Thread t1 = new banker();
t1.setName("线程1");
t1.setDaemon(true);//在线程启动之前将线程设置为守护线程
t1.start();
//主线程是用户线程
for(int i=0;i<10;i++)
{
System.out.println(Thread.currentThread().getName()+"主线程"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class banker extends Thread
{
@Override
public void run() {
int i =0;
while (true)
{
System.out.println(Thread.currentThread().getName()+"数据备份"+(++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 所以在上面这份代码当中纵使线程1是执行了一个死循环,但是他是一个守护线程,当主线程mian执行完10次循环之后,他就需要立即停止。
定时器
-
手撸一个定时器
-
可以通过Timer类当中的schedule()方法获得一个定时器
-
timer.schedule(一个TimerTask类的需要需要执行的任务类定时器开始时间,任务间隔时间); //schedule方法需要传入三个参数,一个TimerTask类的对象,一个定时任务的启动时间,一个任务执行的间隔时间
-
定时器源码
package com.zb.test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/27 13:51
**/
public class ThreadTest13 {
public static void main(String[] args) {
Timer timer = new Timer();
//timer.schedule(一个TimerTask类的需要需要执行的任务类,定时器开始时间,任务间隔时间);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Date firsttime = sdf.parse("2021-10-27 14:04:40");
timer.schedule(new task(),firsttime,1000*2);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
class task extends TimerTask
//TimerTask是一个抽象类,用继承的方式实现他的run方法
{
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String sf = sdf.format(new Date());
System.out.println("该程序在"+sf+"时间:执行了数据备份");
}
}
实现线程的第三种方式
- 实现Callable接口
- 执行步骤:1.先要创建一个未来对象 FutureTask task,给这个对象传一个Callable接口类的对象,下面的代码采用匿名内部类的方式传入
- Callable接口实现call方法,这个call方法相当于之前的run方法,但是这个call方法可以有返回值,而且可以在call方法上抛出异常
- 缺点是:采用这种方式实现多线程
package com.zb.test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;//JUC包下属于并发包,老JDK中没有这个包,这属于是新特性
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/27 14:23
**/
public class ThreadTest14 {
public static void main(String[] args) {
//1、第一步创建一个未来对象,给这个未来对象传一个Callable对象
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("call方法开始");
Thread.sleep(1000*5);
System.out.println("call方法结束");
int a = 1;
int b = 2;
return a+b;
}
});
//创建一个线程对象
Thread t = new Thread(task);
//启动线程
t.start();
try {
//线程执行结果obj
Object obj = task.get();
System.out.println("线程执行结果"+obj);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("mian方法执行结束");
/*这个mian方法执行结束需要等待吗?
需要,因为task.get()这个方法一直在等着拿call方法执行完成后的结果,所以 System.out.println("mian方法执行结束")
这段话的输出需要等待task.get()这个方法执行完毕后才能执行,而get()方法的执行可能需要很长mian方法这里的代码想要执行必须等待*/
}
}
- 注意: 这个mian方法执行结束需要等待吗?需要,因为task.get()这个方法一直在等着拿call方法执行完成后的结果,所以 System.out.println(“mian方法执行结束”)这段话的输出需要等待task.get()这个方法执行完毕后才能执行,而get()方法的执行可能需要很长mian方法这里的代码想要执行必须等待
生产和消费者模式
- 1、wait方法和notify方法不是线程对象的方法,是java种任何一个对象都有的方法,因为这两个方法是Object类中自带的。
- 2、wait方法和notify方法不是通过线程对象调用的,不是t.wait()也不是t.notify()
- 3、wait()方法的作用
Object o = new Object();
o.wait();
表示:让正在o对象上活动的线程进入等待状态,无限期等待,直到被唤醒为止,o.wait();方法的调用,会让"当前线程(正在o对象上活动的线程)"进入等待状态。
- 4、notify()方法的作用
Object o = new Object();
o.notify();
表示:唤醒正在o对象上等待的线程。
还有一个notifyAll()方法:作用是唤醒o对象上处于等待的所有线程
- 生产者模式和消费者模式结构图
- 使用wait方法和notify方法实现生产者模式和消费者模式
package com.zb.test;
import java.util.ArrayList;
import java.util.List;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/27 16:14
**/
public class ThreadTest15 {
public static void main(String[] args) {
//创建一个list仓库集合
List list = new ArrayList();
//创建生产者线程
Thread t1 = new Thread(new producer(list));
//创建消费者线程
Thread t2 = new Thread(new consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
//启动t1线程
t1.start();
//启动t2线程
t2.start();
}
}
//创建一个生产者线程
class producer implements Runnable{
//一个List属性,将来是一个共享的仓库
private List list;
//通过构造器注入需要共享的对象
public producer(List list) {
this.list = list;
}
@Override
public void run() {
//死循环让生产者一直生产
while(true)
{
synchronized(list)
{
//如果说list仓库是大于0的说明仓库中已经存放有东西了,这时候生产者线程应该进入等待
if (list.size()>0)
{
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果list仓库中的元素为0,说明生产者线程应该向仓库中生产东西了
else if (list.size()==0){
Object o = new Object();
list.add(o);
System.out.println(Thread.currentThread().getName()+"生产了"+o);
}
//代码如果执行到这说明生产者线程对于仓库要么操作不了要么是已经操作完成,所以此时应该唤醒消费者消费了
//list.notifyAll();
list.notify();
}
}
}
}
//创建一个消费者线程
class consumer implements Runnable{
//一个List属性,将来是一个共享的仓库
private List list;
//通过构造器注入需要共享的对象
public consumer(List list) {
this.list = list;
}
@Override
public void run() {
//死循环让消费者一直消费
while(true)
{
synchronized (list)
{
//如果消费者线程在进行消费的时候发现list仓库中没有东西,说明消费者应该等待一下
if (list.size()==0)
{
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果消费者线程在进行消费的时候发现仓库中有东西那我就消费它
else if(list.size()>0)
{
System.out.println(Thread.currentThread().getName()+"消费了"+list.get(0));
list.remove(0);
}
//代码如果执行到这说明消费者线程对于仓库要么操作不了要么是已经操作完成,所以此时应该唤醒生产者生产了
//list.notifyAll();
list.notify();
}
}
}
}