Javase 详解 多线程:彻底搞懂线程

  • ​​1.进程、线程(难度:⭐⭐⭐)​​
  • ​​1.1什么是进程?什么是线程?​​
  • ​​1.2(举例)对于Java程序来说,什么是进程?什么是线程?​​
  • ​​1.2.进程和线程是什么关系?举个例子​​
  • ​​1.2.1注意:​​
  • ​​1.3.思考一个问题:​​
  • ​​1.3.1一个线程一个栈 [ 图 文 ]​​
  • ​​1.4.对于单核的CPU来说,真的可以做到真正的多线程并发吗?​​
  • ​​1.5.分析程序有几个线程​​
  • ​​2.实现线程的两种方式(难度:⭐⭐⭐)​​
  • ​​2.1.继承Thread​​
  • ​​2.2.实现Runnable接口实现Run方法​​
  • ​​2.2.1采用匿名内部类可以吗?​​
  • ​​2.3.run方法和start的区别​​
  • ​​2.4.线程的生命周期​​
  • ​​3.线程的一些内置方法(难度:⭐⭐⭐)​​
  • ​​3.1如何设置/获取线程的名字​​
  • ​​3.2.线程睡眠​​
  • ​​3.2.2sleep睡眠方法的面试题​​
  • ​​3.3.终止线程的睡眠​​
  • ​​3.4.线程的终止方法​​
  • ​​3.4.1.stop方法​​
  • ​​3.4.2.stop方法​​
  • ​​3.5.线程调度​​
  • ​​3.5.1常见的线程调度模型有哪些?​​
  • ​​3.5.2.Java中提供了哪些方法是和线程调度有关系的呢?​​
  • ​​4.线程安全问题(重点:⭐⭐⭐⭐⭐)​​
  • ​​4.1为什么这个是重点?​​
  • ​​4.2.什么情况下数据在多线程并发的情况下,存在线程安全问题​​
  • ​​4.3.怎么解决线程安全问题呢?​​
  • ​​4.4.线程同步,两个专业术语​​
  • ​​4.5模拟线程安全问题。​​
  • ​​4.5.1编写程序模拟两个线程同时对同一个账户进行取款操作。​​
  • ​​4.5.2.同步代码块synchronized​​
  • ​​4.5.2.1对synchronized的理解​​
  • ​​4.5.2.2.Java中有三大变量(线程安全问题)?​​
  • ​​4.5.2.3在实例方法上可以使用synchronized吗?可以的​​
  • ​​4.5.3.synchronized总结​​
  • ​​4.5.4.synchronized面试题​​
  • ​​4.5.5.死锁演示​​
  • ​​4.5.6.开发中怎么解决线程安全问题​​
  • ​​5.线程剩余内容(难度:⭐⭐⭐)​​
  • ​​5.1.守护线程​​
  • ​​5.2.定时器​​
  • ​​5.3.实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)​​
  • ​​5.3.1.关于分支栈开启Callable会不会造成main栈堵塞的问题​​
  • ​​5.4.关于Object类中的wait方法和notify方法。(生产者和消费者模式!)​​

1.进程、线程(难度:⭐⭐⭐)

1.1什么是进程?什么是线程?

  • 进程是一个应用程序(1个进程是一个软件)。
  • 线程是一个进程中的执行场景/执行单元。
  • 一个进程可以启动多个线程。

1.2(举例)对于Java程序来说,什么是进程?什么是线程?

  1. 当在DOS命令窗口中输入:Java Helloword 回车之后。
  2. 首先会先启动JVM,而JVM就是一个进程。
  3. JVM再启动一个主线程调用main方法。
  4. 同时再启动一个垃圾回收线程负责看护,回收垃圾。
  5. 最起码,现在的Java程序中至少有两个线程并发,
  6. 一个是垃圾回收线程,一个是执行main方法的主线程。

1.2.进程和线程是什么关系?举个例子

  • 阿里巴巴:进程
  • 马云:阿里巴巴的一个线程
  • 保安:阿里巴巴的一个线程
  • 京东:进程
  • 强东:京东的一个线程
  • 妹妹:京东的一个线程
  • 进程可以看做是现实生活中的公司
  • 线程可以看作是公司中的某个员工
1.2.1注意:
  • 进程A和进程B的内存独立不共享(阿里巴巴和京东内存不会共享的!)
  • 线程A和线程B呢?
  • 在Java语言中:
  • 线程A和线程B,堆内存和方法区内存共享。
  • 但是栈内存独立,一个线程一个栈。
  • 假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行小各自的,这就是多线程并发。
  • 火车站中的每一个售票窗口可以看作是一个线程。
  • 我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
  • 所以多线程并发可以提高效率。
  • Java中之所以有多线程机制,目的就是为了提高程序的处理效率。

1.3.思考一个问题:

使用了多线程之后,main方法结束,是不是有 不会结束。

main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈。

1.3.1一个线程一个栈 [ 图 文 ]

详解Javase  多线程:彻底搞懂线程_多线程

1.4.对于单核的CPU来说,真的可以做到真正的多线程并发吗?

  1. 对于多核的CPU电脑来说,真正的多线程并发是没有问题的。
  • 4核CPU表示同一时间点上,可以真正的有4个进程并发执行
  1. 什么是真正的多线程并发?
  • t1线程执行t1的
  • t2线程执行t2的
  • t1不会影响t2,t2也不会影响t1,这叫做真正的多线程并发。
  1. 单核的CPU表示只有一个大脑
  • 不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
  • 对于单核CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人的感觉是:多个事情同时在做!!

1.5.分析程序有几个线程

package com.newXianCheng.XianC01;
/**
* @Description: 分析有几个线程
* @auther:Li Ya Hui
* @Time:2021年5月10日上午10:40:54
*/
public class Test01 {
public static void main(String[] args) {
System.out.println("main begin");
m1();
System.out.println("main over");
}
private static void m1()
{
System.out.println("m1 begin ");
m2();
System.out.println("m1 over ");
}
private static void m2()
{
System.out.println("m2 begin ");
m3();
System.out.println("m2 over ");
}
private static void m3()
{
System.out.println("m3 execute!");
}
}
  1. 只有一个线程 主栈
  2. 没有启动分支栈,没有启动分支线程
  3. 所以这个只有一个主线程

2.实现线程的两种方式(难度:⭐⭐⭐)

(其他后期再补充)

2.1.继承Thread

java支持多线程机制,并且Java已经实现了,我们只需要继承就行了

  • 第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法
package com.newXianCheng.ThreadTest02;
/**
* @Description: 实现线程的第一种方式
* 编写一个类,直接继承java.lang.thread 重写run方法
*
* 怎么创建线程对象?
* 怎么启动线程呢?
* @auther:Li Ya Hui
* @Time:2021年5月12日上午8:36:11
*/
public class Test01 {
public static void main(String[] args) {
//这里是main方法,这里的代码属于主线程,在主栈中运行
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
//myThread.run(); //不会启动线程,不会分配新的分支栈。(这种方式就是单线程)
//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{
@Override
public void run() {
//编写程序,这里程序运行在分支线程中(分支栈)。
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程"+i);
}
}
}

2.2.实现Runnable接口实现Run方法

实现线程的第二种方式,编写一个类实现java.lang.Runnable接口

//定义一个可运行的类
class MyRunnable implements Runnable{
public void run() {
}
}
//创建线程对象
Thread t = new Thread(new MyRunnable());
//启动线程
t.start
package com.newXianCheng.RunnableTesto1;
/**
* @Description:实现线程的第二种方式 java.lang.Runnable接口
* @auther:Li Ya Hui
* @Time:2021年5月12日上午10:42:58
*/
public class Test01 {
public static void main(String[] args) {
//创建一个可运行对象
MyRunnable myRunnable = new MyRunnable();

//将可运行的对象封装到一个线程对象
Thread t = new Thread(myRunnable);
//启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
//这并不是一个线程类,是一个可运行的类,他还不是一个线程类。
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}

注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更灵活。

2.2.1采用匿名内部类可以吗?
package com.newXianCheng.RunnableTesto1;
/**
* @Description: 采用匿名内部类可以吗?
* @auther:Li Ya Hui
* @Time:2021年5月12日上午11:36:02
*/
public class Test02 {
public static void main(String[] args) {
//创建线程对象,采用匿名内部类方式
Thread t = new Thread(new Runnable() {
@Override
public void run() {

}
});
}
}

2.3.run方法和start的区别

  • myThread.run(); //不会启动线程,不会分配新的分支栈。(这种方式就是单线程)
  • start()方法的作用是:启动一个分支线程,在jvm中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
  • 这段代码的任务只是为了开启一个新的栈空间出来,start方法就结束了,线程就启动成功了。
  • 启动成功的线程会自动调用run方法,并且run方法在分支线程的栈底部(压栈)
  • run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的。

run方法运行图

详解Javase  多线程:彻底搞懂线程_多线程_02

2.4.线程的生命周期

  • 新建状态
  • 就绪状态
  • 运行状态
  • 阻塞状态
  • 死亡状态
  • 详解Javase  多线程:彻底搞懂线程_数据安全_03

3.线程的一些内置方法(难度:⭐⭐⭐)

3.1如何设置/获取线程的名字

  • 获取当前线程对象?

static Thread.currentThread()

class MyThread02 extends Thread{
public void run()
{
for (int i = 0; i < 100; i++) {
//获取当前线程的对象
System.out.println(Thread.currentThread());
}
}
}
  • 获取线程对象的名字

String name = 线程对象。getName();

  • 修改线程对象名字

线程对象.setName(“线程名字”);

  • 当线程没有设置名字的时候,默认的名字有什么规律?(了解一下)

Thread-0

Thread-1

Thread-2

package com.newXianCheng.ThreadTest02;
/**
* @Description: 怎么获取当前线程对象
* 怎末获取对象的名字
* 修改线程的名字
* @auther:Li Ya Hui
* @Time:2021年5月12日下午7:52:37
*/
public class Test02 {
public static void main(String[] args) {
//创建线程对象
MyThread02 myThread02 = new MyThread02();
//设置线程的名字
myThread02.setName("tttt");
//获取线程的名字
String name = myThread02.getName();
System.out.println(name);
//启动线程
myThread02.start();
}
}
class MyThread02 extends Thread{
public void run()
{
for (int i = 0; i < 100; i++) {
System.out.println("分支线程-->"+i);
}
}
}

3.2.线程睡眠

  • static void sleep(long millis)
  • 静态方法 sleep(1000);
  • 参数是毫秒
  • 作用:让当前的线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用。
  • 这行代码出现在A线程,A线程进入睡眠
class MyThread02 extends Thread{
public void run()
{
for (int i = 0; i < 100; i++) {
//获取当前线程的对象
System.out.println(Thread.currentThread().getName());
try {
//让当前线程每次循环运行睡眠1秒
Thread.sleep(1000*1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
3.2.2sleep睡眠方法的面试题

为什么分支线程的睡眠方法会让主线程睡眠,因为sleep是静态方法

package com.newXianCheng.ThreadTest02;
/**
* @Description: 关于Thread.slppe的一个面试题
* @auther:Li Ya Hui
* @Time:2021年5月12日下午9:46:25
*/
public class SleepExam {
public static void main(String[] args) throws InterruptedException {
//创建线程对象
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(1);
}
}
});
//尽管是分支线程调用的睡眠,但是因为 sleep是 static
Thread.sleep(1000);
}
}

3.3.终止线程的睡眠

sleep睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么叫醒一个正在睡眠的线程呢?

重点:

  • run()方法当中的异常不能throws ,只能try catch
  • 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。

语法

package com.newXianCheng.ThreadTest02;
/**
* @Description: 唤醒正在睡眠的线程
* @auther:Li Ya Hui
* @Time:2021年5月12日下午9:46:25
*/
public class SleepExam {
public static void main(String[] args) throws InterruptedException {
//创建线程对象
MyThread03 myThread03 = new MyThread03();
Thread thread = new Thread( myThread03);
//尽管是分支线程调用的睡眠,但是因为 sleep是 static
thread.start();
//唤醒线程
thread.interrupt();
}
}
class MyThread03 extends Thread{
public void run()
{
try {
//让当前线程睡眠一年
Thread.sleep(1000*60*60*24*365);

System.out.println("s");
} catch (InterruptedException e) {
System.out.println("线程被中断");
}
}
}

3.4.线程的终止方法

3.4.1.stop方法
  • 缺点容易造成数据损坏(不推荐使用)
//终止线程,缺点容易造成数据丢失
thread.stop();
3.4.2.stop方法
  • 设置一个布尔标记
  • 什么时候想终止,直接改布尔为 false 就可以
package com.newXianCheng.ThreadTest02;
/**
* stop方法不推荐使用
* @Description: 怎末合理的终止一个线程 这种方式是很常用的
* @auther:Li Ya Hui
* @Time:2021年5月13日下午3:29:11
*/
public class Test03 {
public static void main(String[] args) {
//任务
MyRunnable03 myRunnable03 = new MyRunnable03();
//线程类
Thread t = new Thread(myRunnable03);
//线程启动
t.start();
try {
//线程睡眠三秒后
Thread.sleep(3000);
} catch (InterruptedException e) {
}
//终止线程 想要什么时候终止线程t的执行,那么你把标记修改为false,就结束了
myRunnable03.run= false ;
}
}
class MyRunnable03 implements Runnable {
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++)
{
if(run==true)
{
try
{
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" ");
}
catch (InterruptedException e)
{

}
}
}
}
}

3.5.线程调度

3.5.1常见的线程调度模型有哪些?
  • 抢占式调度模型
  • 哪个线程的优先级比较高,抢到的cpu时间片的概率就高一些/多一些
  • java采用的就是抢占式调度模型
  • 均分式调度模型
  • 平均分配cpu时间片,每个线程占有的cpu时间片时间长度一样。
  • 平均分配,一切平等。
  • 有一些编程语言,线程调度模型采用的是这种方式
3.5.2.Java中提供了哪些方法是和线程调度有关系的呢?
  • 线程优先级
  • 线程优先级越高,获得 CPU 时间片的概率就越大,但线程优先级的高低与线程的执行顺序并没有必然联系
  • void setPriority(int newPriority) 设置线程的优先级
  • int getPriority()获取线程优先级
  • 最低优先级1
  • 默认优先级5
  • 最高优先级10
  • 优先级比较高的获取cpu时间片可能会多一些(但也不完全是,大概率是多的)
  • 语法
package com.newXianCheng.ThreadTest02;
/**
* @Description: 线程优先级的使用与讲解 优先级指的是 处于运行状态的时间多一些
* @auther:Li Ya Hui
* @Time:2021年5月13日下午4:46:43
*/
public class Test04 {
public static void main(String[] args) {
//线程静态成员变量
System.out.println("最高优先级"+Thread.MAX_PRIORITY);
System.out.println("最低优先级"+Thread.MIN_PRIORITY);
System.out.println("默认优先级"+Thread.NORM_PRIORITY);

//获取当前线程对象,获取当前线程的优先级
Thread curreThread = Thread.currentThread();
//main线程优先级默认是5
System.out.println(curreThread.getName() + "线程的默认优先级是:"+curreThread.getPriority());
//创建分支线程
Thread t = new Thread(new MyRunnable4());
//调整分支线程优先级
t.setPriority(10);
//调整main线程优先级
Thread.currentThread().setPriority(1);
//启动分支线程
t.start();
//优先级较高的,只是抢到的CPU时间片相对多一些
//大概率方向更偏向于优先级比较高的
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class MyRunnable4 implements Runnable{
@Override
public void run() {
//获取线程优先级
// System.out.println(Thread.currentThread().getName() + "线程的默认优先级是:"+Thread.currentThread().getPriority());
for (int i = 0; i < 300; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
  • 让位方法
  • static void yield()让位方法
  • 暂停当前正在执行的线程对象,并执行其他线程
  • yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用。
  • yield ( )方法的执行会让当前从“运行状态”回到就绪状态。
  • 注意:再回到就绪之后,有可能还会再次抢到。
  • 语法
package com.newXianCheng.ThreadTest02;
/**
* @Description: 让位,当前线程暂停,回到就绪状态,让给其他线程。
* 静态方法:thrad.yield():
* @auther:Li Ya Hui
* @Time:2021年5月13日下午5:18:23
*/
public class Test05 {
public static void main(String[] args) {

Thread t = new Thread(new MyRunnable5());
t.setName("分支线程1");
t.setPriority(1);
t.start();

for (int i = 1; i < 10000; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
for (int i = 1; i < 10000; i++) {
if (i%100 == 0)
{
// 当前线程暂停一下,让给主线程。
//yield方法不是阻塞方法,让当前线程让位,让给其他线程使用。
//会让当前线程从运行状态回到就绪状态 回到就绪之后,有可能还会再次抢到。
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
  • 合并线程
  • void join ()合并线程
class MyThread extends Thread{
public void doSome()
{
MyThread2 t = new MyThread2();
t.join();//当前线程进入阻塞状态,t线程执行,直到t线程结束。当前线程才可以继续执行
}
}

4.线程安全问题(重点:⭐⭐⭐⭐⭐)

4.1为什么这个是重点?

  • 以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都 已经实现了。这些代码我们都不需要编写。
  • 最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据再多线程并发的环境下是否是安全的。

4.2.什么情况下数据在多线程并发的情况下,存在线程安全问题

三个条件:

  • 条件1:多线程并发。
  • 条件2:有共享数据。
  • 条件3:共享数据有修改的行为。

4.3.怎么解决线程安全问题呢?

当多线程并发的情况下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?

线程同步机制

  • 线程排队执行(不能并发)。
  • 用排队执行解决线程安全问题。
  • 这种机制被称为:线程同步机制

怎末解决线程安全问题呀?

  • 使用“线程同步机制”

线程同步就是线程排队了,县城排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

4.4.线程同步,两个专业术语

  1. 异步编程模型:
  • 线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率更高。)
  • 异步就是并发。
  1. 同步编程模型:
  • 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者或在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。
  • 同步就是排队。

4.5模拟线程安全问题。

4.5.1编写程序模拟两个线程同时对同一个账户进行取款操作。

账户类

package com.newXianCheng.ThreadSafe;
/**
* @Description: 银行账户类
* @auther:Li Ya Hui
* @Time:2021年5月14日上午9:25:19
*/
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 void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款
public void withdarw(double money)
{
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before-money;
//模拟网络延迟 一定出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
}

取款的线程

package com.newXianCheng.ThreadSafe;
/**
* @Description: 取款的线程
* @auther:Li Ya Hui
* @Time:2021年5月14日上午10:54:38
*/
public class AccountThread extends Thread{
//两个线程必须共享同一个账户对象。
private Account act ;
//通过构造方法传递过来账户对象
public AccountThread(Account act) {
super();
this.act = act;
}
@Override
public void run() {
//run方法的执行表示取款操作
double money = 5000;
//取款
//多线程并发执行这个方法
act.withdarw(money);
System.out.println("账户"+act.getActno()+"取款成功,余额"+act.getBalance());
}
}

取款的测试类

package com.newXianCheng.ThreadSafe;
/**
* @Description: 测试类 测试取款操作
* @auther:Li Ya Hui
* @Time:2021年5月14日上午10:20:50
*/
public class Test {
public static void main(String[] args) {
Account act = new Account("act-001",20000);

AccountThread t1 = new AccountThread(act);
AccountThread t2 = new AccountThread(act);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
}
}

输出结果:出现问题

账户act-001取款成功,余额15000.0账户act-001取款成功,余额15000.0
4.5.2.同步代码块synchronized

线程同步机制的语法是:

  • synchronized后面小括号中传的这个“数据”是相当关键的。
  • 这个数据必须是多线程共享的数据。才能达到多线程排队。
  • ()中写什么?
  • 需要看你想让那些线程同步。
  • 假设t1,t2,t3,t4,t5 5个线程
  • 你只希望t1,t2,t3排队,t4,t5不需要排队,怎么办?
  • 你一定要在()中写一个t1,t2,t3共享的对象。而这个对象对于t4,t5来说,不是共享的。
synchronized(){ //线程同步代码块}

这里共享的对象是:账户对象。

账户对象是共享的,那么this就是账户对象!

不一定是this,这里只要是多线程共享的那个对象就行

在Java语言中,任何一个对象都有‘一把锁’,其实这把锁就是标记。(只是把它叫做锁)

100个对象,100把锁

以下代码的执行原理:

  • 假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先,一个后。
  • 假设t1先执行了,遇到了synchronized,这个时候自动找”后面共享对象“的对象锁,找到之后,并占有这把锁,然后执行同步代码块结束,这把锁才会释放。
  • 假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时,t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
  • 这样就达到了线程排队执行
  • 这里需要注意的:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的线程对象所共享的
//取款
public void withdarw(double money)
{
//以下几行代码必须是线程排队的,不能并发。
//一个线程把这里的代码全部执行结束之后,另一个线程才能进来
/*
*线程同步机制的语法是:
synchronized()
{
//线程同步代码块
}
*/
synchronized (this) {
double before = this.getBalance();
double after = before-money;
//模拟网络延迟 一定出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
}
4.5.2.1对synchronized的理解

详解Javase  多线程:彻底搞懂线程_多线程_04

4.5.2.2.Java中有三大变量(线程安全问题)?
  • 实例变量:在堆中。
  • 静态变量:在方法区中。
  • 局部变量:在栈中。

以上三大变量中:

局部变量永远都不会存在线程安全问题。

因为局部变量不共享。(一个线程一个栈)

局部变量在栈中。所以局部变量永远都不会共享。

实例变量在堆中,堆只有一个。

静态变量在方法区中,方法去只有一个。

堆和方法都是多线程共享的,所以可能存在线程安全问题。

局部变量+常量:不会有线程安全问题。

成员变量:可能会有线程安全问题。

4.5.2.3在实例方法上可以使用synchronized吗?可以的
  • synchronized出现在实例方法上,一定锁的是this。没得挑。只能是this。不能是其他的对象了。所以这种方式不灵活。
  • 另外还有一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序员的执行效率降低。所以这种方式不常用。
  • synchronized使用在实例方法上有什么优点? 代码写得少了,节鉴了。
  • 如果共享的对象就是this,并且需要同步的代码块是整个方法体, 建议使用这种方式。

4.2.4.如果使用局部变量的话:

建议使用:StringBuilder。

因为局部变量不存在线程安全问题。选择stringBuilder.

StringBuffer效率比较低。

  • ArratList是非线程安全的。
  • Vector是线程安全的。
  • HashMap HashSet是非线程安全的。
  • Hashtable是线程安全的。
4.5.3.synchronized总结

synchronized有两种写法:

  • 第一种:同步代码块 灵活
synchronized(线程共享对象){ 同步代码块;}
  • 第二种:在实例方法上使用synchronized
    表示共享对象一定是this
    并且同步代码块是整个方法体。
  • 第三种:在静态方法上使用synchronized 表示找类锁。
    类锁永远只有1把。
    就创建了100个对象,那类锁也只有一把。
  • 对象锁:1个对象1把锁,100个对象100把锁。
  • 类锁:100个对象,也可能只是1把类锁。
4.5.4.synchronized面试题
package com.newXianCheng.ThreadSafe3;
/**
*
* @Description: synchronized 面试题: 第二个线程是否运行的方法是否会
* @auther:Li Ya Hui
* @Time:2021年5月16日上午10:46:12
*/
//测试类
public class Exam {
public static void main(String[] args) {
System.out.println(~-12);
System.out.println(~12);
MyClass mc = new MyClass();
MyThread t1 = new MyThread(mc);
MyThread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
//线程类
class MyThread extends Thread
{
private MyClass mClass ;
public MyThread(MyClass myClass) {
this.mClass = myClass;
}
public void run()
{
if(Thread.currentThread().getName().equals("t1"))
{
mClass.doSome();
}else if(Thread.currentThread().getName().equals("t2"))
{
mClass.doOther();
}
}
}
//我的任务类
class MyClass {
public synchronized void doSome()
{
System.out.println("doSome begin");
try {
Thread.sleep(1000*3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
  • 判断t2线程是否会等待t1线程结束
  • 结果证明,当一个方法锁住当前对象时,有线程去运行时,其他没有锁的方法在别的线程去运行时并不会等待前一个线程
4.5.5.死锁演示
package com.newXianCheng.ThreadSafe3.DeadLock;
/**
* @Description:
* 死锁代码要会写
* 一般面试官要求你写。
* 只有会写的,才会在以后的开发中注意这个事儿。
* 因为死锁很难调试
* @auther:Li Ya Hui
* @Time:2021年5月16日下午6:55:47
*/
public class DeadLock {

public static void main(String args[])
{
Object o1 = new Object();
Object o2 = new Object();

Thread t1 = new MyThread1(o1, o2);
Thread t2 = new MyThread2(o1, o2);

t1.start();
t2.start();
}
}
/**
* @Description: 死锁演示
* @auther:Li Ya Hui
* @Time:2021年5月16日下午6:58:42
*/
class MyThread1 extends Thread{
Object o1 ;
Object o2 ;
public MyThread1(Object o1 , Object o2){
this.o1 = o1 ;
this.o2 = o2;
}
public void run()
{
synchronized (o2) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o1) {
System.out.println("1");
}
}
}
}
class MyThread2 extends Thread{
Object o1 ;
Object o2 ;
public MyThread2(Object o1 , Object o2){
this.o1 = o1 ;
this.o2 = o2;
}
public void run()
{
synchronized (o1) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o2) {
System.out.println("2");
}
}
}
}
4.5.6.开发中怎么解决线程安全问题

聊一聊,我们以后开发中应该怎么解决线程安全问题?

  • 是一上来就选择线程同步吗?synchronized
  • 不是,synchronized会让程序的执行效率降低,用户体验不好。
  • 系统的用户吞吐量降低。用户体验极差。在不得已的情况下在选择线程同步机制。

第一种方案:尽量使用局部变量代替“实例变量和静态变量”。

第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)

第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能synchronized了。线程同步机制。

5.线程剩余内容(难度:⭐⭐⭐)

5.1.守护线程

  • Java语言中线程分为两大类:
  • 一类是:用户线程
  • 一类是:守护线程(后台线程)
  • 其中具有代表性的是:垃圾回收线程(守护线程)。
  • 守护线程的特点:
  • 一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
  • 注意:主线程main方法是一个用户线程
  • 守护线程用在什么地方呢?
  • 每天00:00的时候系统数据自动备份。
  • 这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
  • 一直在那里看着,每到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
  • 设置守护线程的语法
  • Thread.setDaemon(true);
package com.newXianCheng.ThreadShouHU;
/**
* @Description: 守护线程
* @auther:Li Ya Hui
* @Time:2021年5月16日下午9:29:31
*/
public class Test01 {
public static void main(String[] args) {
Thread t2 = new bakDatathread();
t2.start();
Thread t = new bakDatathread();
t.setName("备份数据的线程");
//设置守护线程
//启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
//主线程 : 主线程是用户线程
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class bakDatathread extends Thread{
public void run() {
int i = 0;
//即使是死循环,但由于该线程是守护者,当用户线程结束后,守护线程自动终止。
while(true)
{
System.out.println(Thread.currentThread().getName()+" -- "+(++i));
try {
Thread.sleep(1999);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
  • 需要注意的一些知识点:
  • 守护线程–也称“服务线程”,在没有用户线程可服务时会自动离开。
  • 守护线程就是运行在系统后台的线程,如果JVM中只有守护线程,则JVM退出。
  • Main主线程结束了(Non-daemon thread), 如果此时正在运行的其他threads是daemon threads , JVM会使得这个threads停止 , JVM也停下 , 如果此时正在运行的其他threads有Non-daemon threads,那么必须等所有的Non daemon线程结束了,JVM才会停下来。
  • 必须等所有的Non-daemon线程都运行结束了,只剩下daemon的时候,JVM才会停下来,注意Main主程序是Non-daemon线程.
  • 线程划分为用户线程和后台(daemon)进程,setDaemon将线程设置为后台进程
  • 典型的守护线程例子是JVM中的系统资源自动回收线程, 我们所熟悉的Java垃圾回收线程就是一个典型的守护线程。
  • setDarmon()方法在start()方法之前。
  • setDaemon方法把java的线程设置为守护线程,此方法的调用必须在线程启动之前执行。

5.2.定时器

  • 定时器的作用
  • 间隔特定的时间,执行特定的程序。
  • 每周要进行银行账户的总帐操作。
  • 每天要进行数据的备份操作。
  • 在实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的,那么在Java中其实可以采用很多中方式实现:
  • 可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)
  • 在Java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中很少用,因为现在有很多高级框架都是支持定时任务的。
  • 在实际的开发当中,目前使用较多的是spring框架中提供的springTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

5.3.实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)

  • 这种方式实现的线程可以获取线程的返回值。
  • 之前讲解的那两种方式是无法获取线程的返回值的,因为run方法返回void。
  • 思考:
  • 系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?
  • 使用第三种方式:实现Callable接口方式。
  • 语法
package com.newXianCheng.CallableTest;
/**
* @Description: 实现线程的第三种方式:
* 实现Callable接口
* 这种方式的优点:可以获取到线程的执行结果。
* 这种方式的缺点:效率比较低,在获取t线程的时候,当前线程受阻塞,效率较低。
* @auther:Li Ya Hui
* @Time:2021年5月19日上午11:13:56
*/
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest01 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//第一步,创建一个“未来任务类”对象。
//参数非常重要:效率比较低,在获取t线程的时候,当前线程受阻塞,效率较低。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
//线程执行一个任务 , 执行之后可能会有一个执行结果
//模拟执行
System.out.println("call method begin");
Thread.sleep(1000*3);
System.out.println("call method end");
int a=100;
int b=100;
return a+b; // (自动装箱 integer)
}
});
//创建线程对象
Thread t =new Thread(task);
//启动线程
t.start();
//现在是main方法,主线程中
//在主线程中,怎么获取t线程的返回结果?
System.out.println(task.get());

//main方法这里的程序要想执行必须等待get方法的结束
//而get()方法可能需要很久。因为get方法是为了那另一个线程的执行结果
//另一个线程执行是需要时间的。
}
}
5.3.1.关于分支栈开启Callable会不会造成main栈堵塞的问题

详解Javase  多线程:彻底搞懂线程_高并发_05

  • 回答:不会,c1线程的get方法只会对t1线程造成堵塞,不会对main方法造成影响

5.4.关于Object类中的wait方法和notify方法。(生产者和消费者模式!)

  • wait和notify方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,因为这两个方式是object类中自带的。
  • wait方法和notify方法不是通过线程对象调用的,
  • 不是这样的:t.wait(),也不是这样的:t.notify()…不对。
  • wait方法的作用?
  • Object o = new Object();
  • o.wait();
  • 表示:
  • 让正在o对象上活动的线程进入等待状态,无限期待等待,直到唤醒为止
  • o.wait;方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。
  • notify()方法作用?
  • Object o = new Object();
  • o.notify();
  • 表示:
  • 唤醒正在o对象上等待的线程。
  • 还有一个notifyAll()方法:
  • 这个方法是唤醒o对象上处于等待的所有线程。
  • 重点:
  • o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁。
  • o.notify()方法只会通知,不会释放之前占有的o对象的锁。
  • 生产者和消费者 是为了专门解决某个特定需求的。
  • 一个线程负责生产 、 一个线程负责消费
  • 最终要达到生产和消费必须均衡。
  • 例如:
  • 生产满了,就不能再继续生产了,必须让消费线程进行消费。
  • 消费完了,就不能再消费了,必须让生产线程进行生产。
  • 仓库是多线程共享,所以需要考虑仓库的线程安全问题。
  • 仓库对象最终调用wait()和notify()方法。
  • wait()和notify()建立在synchronized线程同步的基础之上。
  • 代码
package com.newXianCheng.producer;

import java.util.ArrayList;
import java.util.List;
/**
* @Description:
* 1.使用wait方法和notify方法实现“生产者和消费者模式”
* 2.什么是“生产者和消费者模式”?
* 生产者负责生产,消费线程负责消费。
* 生产线程和消费线程要达到均衡。
* 这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。
* 3.wait和notify方法不是线程对象的方法,是普通Java对象都有的方法。
* 4.wait方法和nitify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库,有线程安全问题。
* 5.wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。
* 6.notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁
* 7.模拟这样一个需求:
* 仓库我们采用List集合。
* List集合中假设只能存储1个元素。
* 1个元素就表示仓库满了。
* 如果List中元素个数是0,就表示仓库空了。
* 保证List中永远都是最多存储1个元素。
*
* @auther:Li Ya Hui
* @Time:2021年5月19日下午8:30:34
*/
public class Test {
public static void main(String[] args) {
//创建仓库对象
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) {
//给仓库list加锁
synchronized (list) {
if (list.size()>0) {
//当前线程进入等待状态,并且释放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) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//程序执行到此处说明仓库中有数据,进行消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
//唤醒生产者,进行生产
list.notify();
}
}
}
}