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

Source

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

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一个线程一个栈 [ 图 文 ]

在这里插入图片描述

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

  1. 对于多核的CPU电脑来说,真正的多线程并发是没有问题的。
    • 4核CPU表示同一时间点上,可以真正的有4个进程并发执行
  2. 什么是真正的多线程并发?
    • t1线程执行t1的
    • t2线程执行t2的
    • t1不会影响t2,t2也不会影响t1,这叫做真正的多线程并发。
  3. 单核的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方法运行图
在这里插入图片描述

2.4.线程的生命周期

  • 新建状态

  • 就绪状态

  • 运行状态

  • 阻塞状态

  • 死亡状态
    在这里插入图片描述

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,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率更高。)
    • 异步就是并发。
  2. 同步编程模型:
    • 线程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的理解

在这里插入图片描述

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,可以直接拿来用。不过,这种方式在目前的开发中很少用,因为现在有很多高级框架都是支持定时任务的。

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

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