JAVA初步学习——第十章 线程

一、进程和线程

  1. 进程
  • 进程代表了内存中正在运行的应用程序,计算机中的资源(cpu 内存 磁盘 网络等),会按照需求分配给每个进程,从而这个进程对应的应用程序就可以使用这些资源了。
  • 进程就是在系统中,运行一个应用程序的基本单位。
  1. 线程
  • 线程是进程中的一个代码执行单元,负责当前进程中代码程序的执行
  • 一个进程中有一个或多个线程。

二、并发和并行

  • 线程的并发执行,是指在一个时间段内,俩个或多个线程,使用一个CPU,进行交替运行。
  • 线程的并行执行,是指在同一时刻,俩个或多个线程,各自使用一个CPU,同时进行运行。
    • 如果计算机是单核CPU的话,那么同一时刻只能有一个线程使用CPU来执行代码
    • 如果计算机是多核CPU的话,那么同一时刻有可能是俩个线程同时使用不同的CPU执行代码

三、时间片

  1. 时间片
  • 时间片,当前一个线程要使用CPU的时候,CPU会分配给这个线程一小段时间(毫秒级别),这段时间就叫做时间片。
    ————也就是该线程允许使用CPU运行的时间,在这个期间,线程拥有CPU的使用权。
  • 如果在一个时间片结束时,线程还在运行,那么这时候,该线程就需要停止运行,并交出CPU的使用权,然后等待下一个CPU时间片的分配。
  • 俩个线程在使用一个CPU的时候,它们是交替着运行的,每个线程每次都是运行一个很小的时间片,然后就交出CPU使用权
  1. 调度方式
  • 时间片轮转
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    JVM中的线程,使用的为抢占式调度。
    系统会让优先级高的线程优先使用 CPU(提高抢占到的概率),但是如果线程的优先级相同,那么会随机选择一个线程获取当前CPU的时间片。
public static void main(String[] args) { 
	//创建线程对象t1 
	Thread t1 = new Thread(){ 
		@Override 
		public void run() {
			for (int i = 0; i < 100; i++) { 
				System.out.println("hello"); 
			} 
		} 
	};
	
	//创建线程对象t2 
	Thread t2 = new Thread(){ 
		@Override 
		public void run() {
			for (int i = 0; i < 100; i++) { 
				System.out.println("world"); 
			} 
		} 
	};
	
	//启动线程t1 t2 
	t1.start(); 
	t2.start(); 
}

四、main线程

Thread.currentThread(); 可以写在任意方法中,返回就是执行这个方法的线程对象

public class Test { 
	public static void main(String[] args) { 
		
		//获取执行当前方法的线程对象 
		Thread currentThread = Thread.currentThread(); 
		System.out.println("执行当前方法的线程名字为:"+currentThread.getName()); 
	} 
}

//运行结果: 
	执行当前方法的线程名字为:main

五、线程的创建和启动

java.lang.Thread 是java中的线程类,所有的线程对象都必须是Thread类或其子类的实例。
每个线程的作用,就是完成我们给它指定的任务,实际上就是执行一段我们指定的代码。我们只需要在Thread 类的子类中重写 run 方法,把执行的代码写入到run方法中即可,这就是线程的执行任务!

Java中通过继承Thread类来创建并启动一个新的线程的步骤如下:

  1. 定义 Thread 类的子类(可以是匿名内部类),并重写 Thread 类中的 run 方法,run 方法中的代码就是线程的执行任务
  2. 创建 Thread 子类的对象,这个对象就代表了一个要独立运行的新线程
  3. 调用线程对象的 start 方法来启动该线程

(1)定义 Thread 类的子类

public class Test { 
	public static void main(String[] args) { 
		
		//2.创建线程类对象 
		Thread t = new MyThread(); 
		
		//3.调用start方法启动线程 
		t.start(); 
	} 
}
//1.子类继承父类Thread,并重写run方法(指定线程的执行任务) 
class MyThread extends Thread{ 
	@Override 
	public void run() {
		for (int i = 0; i < 10; i++) { 
			System.out.println("hello world"); 
			
			try {
				//可以让当前执行代码的线程睡眠1000毫秒 
				Thread.sleep(1000); 
			} catch (InterruptedException e) { 
				e.printStackTrace(); 
			} 
		} 
	} 
}

(2)匿名内部类

public class Test {
	public static void main(String[] args) { 
		Thread t = new MyThread(){ 
			@Override 
			public void run() {
				for (int i = 0; i < 10; i++) { 
					System.out.println("hello world"); 
					try {
						Thread.sleep(1000); 
					} catch (InterruptedException e) { 
						e.printStackTrace(); 
					} 
				} 
			} 
		};
		t.start(); 
	} 
}

六、Runnable接口

  • 给一个线程对象指定要执行的任务,除了继承Thread类后重写run方法之外,还可以利于Runnable接口来完成线程任务的指定
  • java.lang.Runnable ,该接口中只有一个抽象方法 run
  • Thread 类也是 Runnable 接口的实现类
  • 子类重写Thread中的run方法,这个run方法其实也来自于Runnable接口
  • 实现Runnable接口比继承Thread类所具有的优势:
    • 可以把相同的一个执行任务(Runnable接口的实现),交给不同的线程对象去执行
    • 可以避免java中的单继承的局限性。
    • 线程和执行代码各自独立,实现代码解耦

使用 Runnable 接口的匿名内部类,来指定线程的执行任务(重写接口中的run方法):

public class Test { 
	public static void main(String[] args) { 
		//Runnable接口的实现类中,重写了run方法,指定线程的执行任务 
		Runnable run = new Runnable() { 
			@Override 
			public void run() {
				for (int i = 0; i < 10; i++) { 
					System.out.println("hello world"); 
					
					try {
						Thread.sleep(1000); 
					} catch (InterruptedException e) { 
						e.printStackTrace(); 
					} 
				} 
			} 
		};
		
		//创建线程对象,指定执行任务 
		Thread t = new Thread(run); 
		t.start(); 
	} 
}

七、线程的名字

  1. 获取当前线程的名字
  • 通过Thread类中的currentThread方法,可以获取当前线程的对象,然后调用线程对象的getName方法,可以获取当前线程的名字。 String name = Thread.currentThread().getName();
  • start方法启动线程后,线程会自动执行run方法
  • 千万不要直接调用run方法,这样就不是启动线程执行任务,而是普通的方法调用
public class Test { 
	public static void main(String[] args) { 
		String name = Thread.currentThread().getName(); 
		System.out.println("执行当前main方法的线程是:"+name); 
		
		Runnable run = new Runnable() { 
			@Override 
			public void run() { 
				String name = Thread.currentThread().getName(); 
				System.out.println("执行当前run方法的线程是:"+name); 
			} 
		};
		Thread t = new Thread(run); 
		t.start(); 
	} 
}

//运行结果为: 
	执行当前main方法的线程是:main 
	执行当前run方法的线程是:Thread-0
  1. 线程的默认名字
  • 默认情况下,主线程中,创建出的线程,它们的都会有一个默认的名字
  • "Thread-" + nextThreadNum() 就是在拼接出这个线程默认的名字,Thread-0 Thread-1 Thread-2等等
public Thread() { 
	init(null, null, "Thread-" + nextThreadNum(), 0); 
}
  1. 指定线程的名字
    可以创建线程对象的时候,给它设置一个指定的名字:
Thread t = new Thread("t线程"); 

//或者 
Thread t = new Thread(new Runnable(){ 
	public void run(){ //执行任务 } 
},"t线程"); 

//或者 
Thread t = new Thread(); 
t.setName("t线程");

八、线程的分类

  • 前台线程,又叫做执行线程、用户线程
    • 这种线程专门用来执行用户编写的代码,地位比较高,JVM是否会停止运行,就是要看当前是否还有前台线程没有执行完,如果还剩下任意一个前台线程没有“死亡”,那么JVM就不能停止!
    • 执行程序入口的主线程(main),就是一个前台线程
    • 在主线程创建并启动的新线程,默认情况下就是一个前台线程,用来执行用户编写的代码任务。
  • 后台线程,又叫做守护线程、精灵线程
    • 这种线程是用来给前台线程服务的,给前台线程提供一个良好的运行环境,地位比较低,JVM是否停止运行,根本不关心后台线程的运行情况和状态。

在主线程中,创建出来的线程对象,默认就是前台线程,在它启动之前,我们还可以给它设置为后台线程:

public class Test { 
	public static void main(String[] args) { 
		Thread t = new Thread("t线程"){ 
			@Override 
			public void run() { 
				String name = Thread.currentThread().getName();
				for (int i = 0; i < 10; i++) { 
					System.out.println(name+": hello "+i); 
				} 
			} 
		};
		//在启动线程之前,可以将其设置为后台线程,否则默认是前台线程 
		t.setDaemon(true); 
		t.start(); 
	} 
}

九、线程优先级

  • 最终设置线程优先级的方法,是一个native方法,并不是java语言实现的
  • 线程的优先级使用int类型数字表示,最大是10,最小是1,默认的优先级是5
  • 当俩个线程争夺CPU时间片的时候:
    • 优先级相同,获得CPU使用权的概率相同
    • 优先级不同,那么高优先级的线程有更高的概率获取到CPU的使用权

十、线程组

  • Java中使用 java.lang.ThreadGroup 类来表示线程组,它可以对一批线程进行管理,对线程组进行操作,同时也会对线程组里面的这一批线程操作。
  • 创建线程组的时候,需要指定该线程组的名字。也可以指定其父线程组,如果没有指定,那么这个新创建的线程组的父线程组就是当前线程组。

(1)主线程中,创建一个线程对象,它的线程组默认就是当前线程的线程组

public class Test { 
	public static void main(String[] args) {
		Thread t = new Thread(); 
		ThreadGroup threadGroup = t.getThreadGroup(); 
		System.out.println(threadGroup); 
	} 
}

//运行结果: 
	java.lang.ThreadGroup[name=main,maxpri=10]

(2)只有在创建线程对象的时候,才能指定其所在的线程组,线程运行中途不能改变它所属的线程组

public class Test { 
	public static void main(String[] args) { 
		ThreadGroup group = new ThreadGroup("我的线程组"); 
		Runnable run = new Runnable() { 
			@Override 
			public void run() { 
				try {
					//让线程休眠一会,否则运行太快,死亡太快了 
					Thread.sleep(10000); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				} 
			} 
		};
		Thread t1 = new Thread(group,run,"t1线程"); 
		Thread t2 = new Thread(group,run,"t2线程"); 
		Thread t3 = new Thread(group,run,"t3线程"); 
		//注意,启动后,三个线程都会进行休眠,等run方法运行完就“死亡”了 
		t1.start(); 
		t2.start(); 
		t3.start(); 
		//返回当前线程组中还没有“死亡”的线程个数 
		System.out.println("线程组中还在存活的线程个数为:"+group.activeCount()); 
		//准备好数组,保存线程组中还存活的线程 
		Thread[] arr = new Thread[group.activeCount()]; 
		//将存活的线程集中存放到指定数组中,并返回本次存放到数组的存活线程个数 
		System.out.println("arr数组中存放的线程个数为:"+group.enumerate(arr)); 
		//输出数组中的内容 
		System.out.println("arr数组中的内容为:"+Arrays.toString(arr)); 
	} 
}
//运行结果: 
	线程组中还在存活的线程个数为:3 
	arr数组中存放的线程个数为:3 
	arr数组中的内容为:[Thread[t1线程,5,我的线程组], Thread[t2线程,5,我的线程组], Thread[t3线 程,5,我的线程组]]

十一、线程状态

  • java.lang.Thread.State 枚举类型中(内部类形式),定义了线程的几种状态

  • BLOCKEDWAITINGTIMED_WAITING 这三种都属于线程阻塞,只是触发的条件不同,以及从阻塞状态中恢复过来的条件也不同而已。

  • 线程在这三种情况的阻塞下,都具备相同的特点

    • 线程不执行代码
    • 线程也不参与CPU时间片的争夺
  • 状态描述和解释如下:

线程状态名称描述
NEW新建线程刚被创建,还没调用start方法,或者刚刚调用了start方法,调用start方法不一定"立即"改变线程状态,中间可能需要一些步骤才完成一个线程的启动。
RUNNABLE可运行start方法调用结束,线程由NEW变成RUNNABLE,线程存活着,并尝试抢占CPU资源,或者已经抢占到CPU资源正在运行,这俩种情况的状态都显示为RUNNABLE
BLOCKED锁阻塞线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放。这时候线程B就是处理BLOCKED
WAITING无限期等待一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TIMED_WAITING有限期等待和WAITING状态类似,但是有一个时间期限,时间到了,自己也会主动醒来
TERMINATED终止(死亡)run方法执行结束的线程处于这种状态。
  • 一个线程,经历的最普通的过程如下:
    • 刚创建好的线程对象,就是出于NEW的状态
    • 线程启动后,会出于RUNNABLE状态
    • 其实这个RUNNABLE状态包含俩种情况:
      • 就绪状态,此时这个线程没有运行,因为没有抢到CPU的执行权
      • 运行状态,此时这个线程正在运行中,因为抢到CPU的执行权
    • JavaAPI中并没有定义就绪状态和运行状态,而是把这俩情况统一叫做RUNNABLE(可运行状态),但是一般我们为了能更加清楚的描述问题,会用上就绪状态和运行状态
    • 在线程多次抢到CPU执行权,“断断续续”把run方法执行完之后,就变成了TERMINATED状态(死亡),之所以是“断断续续”的运行,是因为每次抢到CPU执行权的时候,只是运行很小的一个时间片,完了之后还要重新抢夺下一个时间片,并且中间还有可能抢不到的情况
    • 死亡后的线程,不能重新启动
public class Test { 
	public static void main(String[] args) { 
		Thread t1 = new Thread("t1线程"){ 
			@Override 
			public void run() {
				for (int i = 0; i < 10; i++) { } 
			} 
		};
		System.out.println(t1.getState()); 
		//启动t1线程 
		t1.start(); 
		System.out.println(t1.getState()); 
		System.out.println(t1.getState()); 
		System.out.println(t1.getState()); 
		System.out.println(t1.getState()); 
		System.out.println(t1.getState());
		System.out.println(t1.getState()); 
		System.out.println(t1.getState()); 
		System.out.println(t1.getState()); 
	} 
}

//运行结果: 
		注意需要多运行几次,因为可能每次运行的情况不一样 
		NEW 
		RUNNABLE 
		RUNNABLE 
		RUNNABLE 
		RUNNABLE 
		RUNNABLE 
		RUNNABLE 
		TERMINATED 
		TERMINATED

十二、线程类Thread中的方法

1.sleep方法

  • 该静态方法可以让当前执行的线程暂时休眠指定的毫秒数
  • 线程执行了sleep方法后,会从RUNNABLE状态进入到TIMED_WAITING状态
  • 线程所处的是一种阻塞状态,这种阻塞的特点是:阻塞结束后,线程会自动回到RUNNABLE状态
public class Test { 
	public static void main(String[] args) { 
		Thread t1 = new Thread("t1线程"){ 
			@Override 
			public void run() { 
				try {
					//t1线程休眠10毫秒 
					Thread.sleep(10); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				} 
			} 
		};
		
		//t1还没有启动,这里肯定是NEW状态 
		System.out.println(t1.getState()); 
		
		//启动t1线程 
		t1.start(); 
		
		//在循环期间查看t1的状态1000次 
		//这里t1的状态可能是RUNNABLE,也可能是TIMED_WAITING,也可能是TERMINATED
		for (int i = 0; i < 1000; i++) { 
			System.out.println(t1.getState()); 
		} 
	} 
}

//运行结果: 
	可以多运行几次,每次结果可能不一样 
	NEW 
	RUNNABLE 
	RUNNABLE 
	RUNNABLE 
	RUNNABLE 
	RUNNABLE 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	TIMED_WAITING 
	..... 
	RUNNABLE 
	RUNNABLE 
	TERMINATED 
	TERMINATED 
	TERMINATED 
	TERMINATED 
	.....

2. join方法

使用join方法,可以让当前线程阻塞,等待另一个指定的线程运行结束后,当前线程才可以继续运行
(1)使用无参join方法

  • 线程执行了join()方法后,会从RUNNABLE状态进入到WAITING状态
  • t2线程中,调用了t1对象的join方法,那么t2线程就会阻塞,等待t1线程的运行结束,t2线程才能恢复
public class Test { 
	public static void main(String[] args) { 
		
		Thread t1 = new Thread("t1线程"){ 
			@Override 
			public void run() { 
				try {
					//t1线程睡眠1秒钟 
					Thread.sleep(1000); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				}
				System.out.println("t1线程结束"); 
			} 
		};
		
		Thread t2 = new Thread("t2线程"){ 
			@Override 
			public void run() { 
				try {
					//t2线程调用t1.join方法 
					//t2线程进入阻塞状态 
					//t2线程要等到t1线程运行结束,才能恢复到RUNNABLE状态 
					t1.join(); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				}
				System.out.println("t2线程结束"); 
			} 
		};
		
		t1.start(); 
		t2.start(); 
		
		//让主线程休眠500毫秒,目的是为了给t1和t2点时间,让他们俩个线程进入状态 
		try {
			Thread.sleep(500); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		}
		System.out.println(t2.getState());
	} 
}

//运行结果: 
	WAITING 
	t1线程结束 
	t2线程结束

(2)使用有参join方法

  • 线程执行了join(long million)方法后,会从RUNNABLE状态进入到TIMED_WAITING状态
public class Test { 
	public static void main(String[] args) { 
		
		Thread t1 = new Thread("t1线程"){ 
			@Override 
			public void run() { 
				try {
					//t1线程睡眠1秒钟 
					Thread.sleep(1000); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				}
				System.out.println("t1线程结束"); 
			} 
		};
		
		Thread t2 = new Thread("t2线程"){ 
			@Override 
			public void run() { 
				try {
					//t2线程调用t1.join方法 
					//t2线程进入阻塞状态 
					//t2线程要等到t1线程运行结束,才能恢复到RUNNABLE状态 
					//2000表示,当前线程t2最多阻塞2秒钟,2秒钟之内t1线程没有结束,那么t2线 程就自动恢复 	
					t1.join(2000); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				}
				System.out.println("t2线程结束"); 
			} 
		};
		t1.start();
		t2.start(); 
		
		//让主线程休眠500毫秒,目的是为了给t1和t2点时间,让他们俩个线程进入状态 
		try {
			Thread.sleep(500); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		}
		System.out.println(t2.getState()); 
	} 
}
//运行结果: 
	TIMED_WAITING 
	t1线程结束 
	t2线程结束
  • 如果指定了时间,线程阻塞一定的时间后,会自动恢复到RUNNABLE状态,这种情况下,线程的状态为TIMED_WAITING(有限期等待)
  • 如果没有指定时间,线程会一直阻塞着,直到某个条件满足时,才会自动恢复,这种情况下,线程的状态为WAITING(无限期等待)

3.interrupt方法

  • sleep方法和 join 方法可知,这俩个方法都会抛出 InterruptedException 类型的异常
  • InterruptedException 异常类型指的是:线程A中,调用了线程B的interrupt方法,而此时线程B处于阻塞状态,那么此时sleep 方法或者join方法就会抛出被打断的异常
public class Test { 
	public static void main(String[] args) { 
		
		Thread t1 = new Thread("t1线程"){ 
			@Override 
			public void run() { 
				try {
					//t1线程休眠100秒 
					Thread.sleep(100000); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				}
				System.out.println("t1线程结束"); 
			} 
		};
		t1.start(); 
		
		//让主线程休眠500毫秒,目的是为了给t1时间,让它调用sleep方法而进入阻塞状态 
		try {
			Thread.sleep(500); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		}
		//打断t1由于调用sleep方法而进入的阻塞状态 
		t1.interrupt();
	} 
}

//运行结果: 
	java.lang.InterruptedException: sleep interrupted 
		at java.lang.Thread.sleep(Native Method) 
		at com.briup.sync.Test$1.run(Test.java:11) 
	t1线程结束
  • interrupt方法的工作原理:
    interrupt方法是通过改变线程对象中的一个标识的值(true|false),来达到打断阻塞状态的效果。一个线程在阻塞状态下,会时刻监测这个标识的值是不是true,如果一旦发现这个值变为true,那么就抛出异常结束阻塞状态,并再把这个值改为false。
  • 查看线程对象中“打断标识”值的俩个方法:

(1)非静态方法

只是返回这个“打断标识”值,并且不会对这个值进行清除(true- >false),因为所传参数ClearInterrupted的值为false

public class Test { 
	public static void main(String[] args) { 
		Thread t1 = new Thread("t1线程"){ 
			@Override 
			public void run() {
				for (int i = 0; i < 10; i++) { 
					//判断是否有其他线程调用了自己的interrupt方法 
					//调用类中的非静态方法:isInterrupted 
					System.out.println(this.isInterrupted()); 
				}
				System.out.println("t1线程结束"); 
			} 
		};
		t1.start(); 
	}
}//运行结果:
	false
	false
	false
	false
	false
	false
	false
	false
	false
	false 
	t1线程结束

(2)静态方法

返回这个“打断标识”值,并且会对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为true

public class Test { 
	public static void main(String[] args) { 
		Thread t1 = new Thread("t1线程"){ 
			@Override 
			public void run() {
				for (int i = 0; i < 10; i++) { 
					//判断是否有其他线程调用了自己的interrupt方法 
					//调用类中的静态方法:interrupted 
					System.out.println(Thread.interrupted()); 
				}
				System.out.println("t1线程结束"); 
			} 
		};
		t1.start();
		t1.interrupt(); 
	} 
}

//运行结果: 
		true
		false
		false
		false
		false
		false
		false
		false
		false
		false 
		t1线程结束

十三、线程安全

JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。
JVM内存中的栈区,是线程的私有空间,每个线程都有自己的栈区,别的线程无法访问到自己栈区的数据。

如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量,并且有写入的操作,那么最终可能会出数据的结果和预期不符的情况,这种情况就是线程安全问题。

  • t1和t2并发访问的时候,争夺CPU的时间片,运行完时间片,退出后再次争夺下一个时间片,也就是说t1和t2都是“断断续续”的运行的
  • 在这期间,可能t1线程有一次拿到时间片运行的时候,给num赋值为1,然后时间片用完退出了,结果下次t2线程拿到了时间片,又将num的值赋成了11750,然后t1线程又拿到了时间片,本来预期的是输出1,但是结果却是输出了11750
  • 核心的原因是,t1线程操作一下变量num,然后时间片用完退出去,t2先过来又操作了变量num,等t1线程再过来的时候,这值已经被t2线程给“偷偷”修改了,那么就出现了和预期不符的情况
public class Test { 
	public static void main(String[] args) { 
		MyData myData = new MyData(); 
		
		Thread t1 = new Thread("t1"){ 
			@Override 
			public void run() { 
				String name = Thread.currentThread().getName();
				for (int i = 0; i < 10; i++) { 
					//先给num赋值 myData.num = i; 
					//然后再输出num的值 
					System.out.println(name + ": " + myData.num); 
				} 
			} 
		};
		
		Thread t2 = new Thread("t2"){ 
			@Override 
			public void run() {
				for (int i = 100; i < 20000; i++) { 
					//给num赋值 myData.num = i; 
				} 
			} 
		};
		t1.start(); 
		t2.start(); 
	} 
}
class MyData{ 
	int num; 
}

//运行结果:
	t1: 0 
	t1: 11706 
	t1: 13766 
	t1: 15459 
	t1: 17710 
	t1: 19304 
	t1: 6 
	t1: 7 
	t1: 8 
	t1: 9

十四、 线程同步(解决线程安全问题)

  • Java中实现线程同步的方式,是给需要同步的代码进行 synchronized 关键字加锁。
  • 线程同步的效果,就是一段加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到的锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,它才能去拿这个锁然后再运行代码。
  • 本来这段代码是俩线程并发访问,“争先恐后”的去执行的,现在线程同步之后,这段代码就变成了先由一个拿到锁的线程去执行,执行完了,再由另一个线程拿到锁去执行。
public class Test { 
	public static void main(String[] args) { 
		MyData myData = new MyData(); 
		
		Thread t1 = new Thread("t1"){ 
			@Override 
			public void run() { 
				String name = Thread.currentThread().getName(); 
				
				synchronized (myData){
					for (int i = 0; i < 10; i++) { 
						myData.num = i; 
						System.out.println(name + ": " + myData.num); 
					} 
				} 
			} 
		};
		
		Thread t2 = new Thread("t2"){ 
			@Override 
			public void run() { 
				
				synchronized (myData){
					for (int i = 100; i < 20000; i++) { 
						myData.num = i; 
					} 
				} 
			} 
		};
		t1.start(); 
		t2.start(); 
	} 
}

class MyData{ 
	int num; 
}
  • 这时候t2线程的阻塞状态属于锁阻塞,需要等待另一个线程把锁释放了,t2线程才能恢复。如果t2线程处于这种阻塞,那么调用线程对象的 getState 方法返回的状态名称为:BLOCKED
  • t1线程需要拿到锁对象obj,才能运行加锁的代码块
  • t2线程也需要拿到锁对象obj,才能运行加锁的代码块
  • 锁对象obj只有一个,所以t1和t2只能有一个线程先拿到,拿到后执行代码,那么另一个就拿不到了,拿不到就阻塞,此时线程的状态为:BLOCKED
  • 任意一个对象,只要是对象,就可以用来当做,加锁代码块中的锁对象。然后让多个线程,去争抢这把锁就可以了,此时就达到了线程同步的效果,因为只有拿到锁的线程,能执行代码,其 他拿不到的线程就不执行,并且进入阻塞状态。
public class Test { 
	public static void main(String[] args) { 
		Object obj = new Object(); 
		Thread t1 = new Thread("t1"){ 
			@Override 
			public void run() {
				synchronized (obj){ 
					try {
						Thread.sleep(100000); 
					} catch (InterruptedException e) { 
						e.printStackTrace(); 
					} 
				} 
			}
		};
		Thread t2 = new Thread("t2"){ 
			@Override 
			public void run() { 
				synchronized (obj){ } 
			} 
		};
		t1.start(); 
		//主线程休眠1秒钟,给t1线程点时间,让他先拿到锁,然后去休眠100秒 
		try {
			Thread.sleep(1000); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		}
		t2.start(); 
		System.out.println("t1线程状态:"+t1.getState()); 
		System.out.println("t2线程状态:"+t2.getState()); 
	} 
}

//运行结果: 
		t1线程状态:TIMED_WAITING 
		t2线程状态:BLOCKED

十五、synchronized

  • synchronized 修饰一个代码块,并指定谁是锁对象的用法,除此之外,还可以使用 synchronized 直接修饰一个方法,表示这个方法中的所有代码都需要线程同步。
  • synchronized 关键字修饰非静态方法,默认使用 this 当做锁对象,并且不能自己另外指定
  • synchronized 关键字修饰静态方法,默认使用 当前类的Class对象 当做锁对象,并且不能自己另外指定
    这俩中情况的同步效果是一样的,只是锁对象不同而已
  • 可以直接在add方法(非静态方法)上,添加修饰符 synchronized 关键字,表示给这个方法中的所有代码进行线程同步,默认使用的锁对象是 this

该代码表示,拿到锁对象this的线程,才可以进入到add方法中执行代码,代码执行完,会释放锁,这时锁变的可用了,所有需要这把锁的线程都恢复到RUNABLE状态(它们之前在锁阻塞状态),这些线程一起重新争夺CPU执行权,谁先拿到CPU执行权,就会先过去拿到锁,进入代码去执行
线程同步的效果的关键点在于,让t1和t2俩个线程去争夺同一把锁对象

public synchronized void add(int num){ 
	String name = Thread.currentThread().getName(); 
	arr[current] = num; 
	System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]); 
	current++; 
}
public class Test { 
	public static void main(String[] args) { 
		MyData myData = new MyData(); 
		
		Thread t1 = new Thread("t1"){ 
			@Override 
			public void run() {
				for (int i = 0; i < 10; i++) { 
					myData.add(i); 
					//计算机运行10次运行太快了,让它执行慢一些,好观察效果 
					try {
						Thread.sleep(1); 
					} catch (InterruptedException e) { 
						e.printStackTrace(); 
					} 
				} 
			} 
		};
		
		//t2线程的名字前面加个制表符\t,打印的时候好观察 
		Thread t2 = new Thread("\tt2"){ 
			@Override 
			public void run() {
				for (int i = 10; i < 20; i++) { 
					myData.add(i); 
					//计算机运行10次运行太快了,让它执行慢一些,好观察效果 
					try {
						Thread.sleep(1); 
					} catch (InterruptedException e) { 
						e.printStackTrace(); 
					} 
				} 
			} 
		};
		t1.start(); 
		t2.start();
	} 
}

十六、锁对象的三个方法

方法:wait()notify()notifyAll

  • 三个核心点:
    • 任何对象中都一定有这三个方法
    • 只有对象作为锁对象的时候,才可以调用
    • 只有在同步的代码块中,才可以调用
      (1)wait 方法:可以让拿到的锁的线程,即使代码没执行完,也可以把锁立即给释放了
  • 线程调用了wait方法,释放了锁,变为阻塞状态(WAITING),并进入了等待池,等待其他线程唤醒自己或者打断自己,如果有线程调用了notify方法进行了唤醒,或者interrupt方法进行了打断,那么这个线程就会从等待池进入到锁池,而进入到锁池的线程,会时刻关注锁对象是否可用,一旦可用,这个线程就会立刻自动恢复到RUNNABLE状态。

TIMED_WAITINGWAITINGBLOCKED都属于线程阻塞,他们共同的特点是就是线程不执行代码,也不参与CPU的争夺,除此之外,它们还有各自的特点:(重要

  1. 线程运行时,调用sleep或者join方法后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程被打断了、或者指定的时间到了,或者join的线程结束了
  2. 线程运行时,发现锁不可用后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程需要争夺的锁对象变为可用了(别的线程把锁释放了)
  3. 线程运行时,调用了wait方法后,线程先释放锁后,再进入这种阻塞,该阻塞状态可以恢复到BLOCKED状态(也就是阻塞2的情况),条件是线程被打断了、或者是被别的线程唤醒了(notify 方法)
  • 希望的是t1线程中i=5的时候,先释放锁,让t2拿到锁去运行,在t2线程中,当j=15的时候,释放锁,让t1拿到锁去运行:
public class Test { 
	public static void main(String[] args) {
		final Object obj = new Object();
		
		Thread t1 = new Thread("t1"){ 
			@Override 
			public void run() { 
				String name = Thread.currentThread().getName(); 
				synchronized (obj){
					for (int i = 0; i < 10; i++) { 
						System.out.println(name+"线程: i = "+i); 
						if(i==5){ 
							try {
								obj.wait(); 
							} catch (InterruptedException e) { 
								e.printStackTrace(); 
							} 
						} 
					} 
				} 
			} 
		};
		
		Thread t2 = new Thread("t2"){ 
			@Override 
			public void run() { 
				String name = Thread.currentThread().getName(); 
				synchronized (obj){
					for (int j = 10; j < 20; j++) { 
						System.out.println(name+"线程: j = "+j); 
						if(j==15){ 
							try {
								obj.wait(); 
							} catch (InterruptedException e) { 
								e.printStackTrace(); 
							} 
						} 
					} 
				} 
			} 
		};
		t1.start(); 
		t2.start(); 
		//主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法 
		try {
			Thread.sleep(1000); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		}
		System.out.println("t1线程当前的状态为:"+t1.getState()); 
		System.out.println("t2线程当前的状态为:"+t2.getState()); 
	} 
}
//运行结果:
		t1线程: i = 0 
		t1线程: i = 1 
		t1线程: i = 2 
		t1线程: i = 3 
		t1线程: i = 4 
		t1线程: i = 5 
		t2线程: j = 10 
		t2线程: j = 11 
		t2线程: j = 12 
		t2线程: j = 13 
		t2线程: j = 14 
		t2线程: j = 15 
		t1线程当前的状态为:WAITING 
		t2线程当前的状态为:WAITINGk

(2)notify方法:可以在等待池中,随机唤醒一个等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了

public class Test { 
	public static void main(String[] args) {
		final Object obj = new Object(); 
		
		Thread t1 = new Thread("t1"){ 
			@Override 
			public void run() { 
				String name = Thread.currentThread().getName(); 
				synchronized (obj){
					for (int i = 0; i < 10; i++) { 
						System.out.println(name+"线程: i = "+i); 
						if(i==5){ 
							try {
								//在释放锁对象之前,叫醒等待池中等待obj锁对象的线程 
								//意思是告诉对方,我要释放锁了,你准备去抢把 
								obj.notify(); 
								obj.wait(); 
							} catch (InterruptedException e) { 
								e.printStackTrace(); 
							} 
						} 
					}
					//最后在执行完所有代码之前,再叫醒一次,防止等待池中还有其他线程在等待 obj这个锁对象 
					obj.notify(); 
				} 
			} 
		};
		
		Thread t2 = new Thread("t2"){ 
			@Override 
			public void run() { 
			String name = Thread.currentThread().getName(); 
				synchronized (obj){
					for (int j = 10; j < 20; j++) { 
						System.out.println(name+"线程: j = "+j); 
							if(j==15){ 
								try {
									//在释放锁对象之前,叫醒等待池中等待obj锁对象的线程 
									//意思是告诉对方,我要释放锁了,你准备去抢把 
									obj.notify(); 
									obj.wait(); 
								} catch (InterruptedException e) { 
									e.printStackTrace(); 
								} 
							} 
					}
					//最后在执行完所有代码之前,在叫醒一次,防止等待池中还线程在等待obj这个锁对象	
					obj.notify(); 
				} 
			} 
		};
		
		t1.start(); 
		t2.start();
		//主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法 
		try {
			Thread.sleep(1000); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		}
		System.out.println("t1线程当前的状态为:"+t1.getState()); 
		System.out.println("t2线程当前的状态为:"+t2.getState()); 
	} 
}
//运行结果: 
		t1线程: i = 0 
		t1线程: i = 1 
		t1线程: i = 2 
		t1线程: i = 3 
		t1线程: i = 4 
		t1线程: i = 5 
		t2线程: j = 10 
		t2线程: j = 11 
		t2线程: j = 12 
		t2线程: j = 13 
		t2线程: j = 14 
		t2线程: j = 15 
		t1线程: i = 6 
		t1线程: i = 7 
		t1线程: i = 8 
		t1线程: i = 9 
		t2线程: j = 16 
		t2线程: j = 17 
		t2线程: j = 18 
		t2线程: j = 19 
		t1线程当前的状态为:TERMINATED 
		t2线程当前的状态为:TERMINATED

(3)notifyAll() 方法:可以在等待池中,唤醒所有等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了.

十七、死锁

  • 在程序中要尽量避免出现死锁情况,一旦发生那么只能手动停止JVM的运行,然后查找并修改产生死锁的问题代码
  • 死锁就是:俩个线程t1和t2,t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释放,俩个线程就这样一直僵持下去。
public class ThreadDeadLock extends Thread{ 
	private Object obj1; 
	private Object obj2; 
	public ThreadDeadLock(Object obj1,Object obj2) { 
		this.obj1 = obj1; 
		this.obj2 = obj2; 
	}
	public void run() { 
		String name = Thread.currentThread().getName(); 
		if("Thread-0".equals(name)){ 
			while(true){ 
				synchronized (obj1) { 
					synchronized (obj2) { 
						System.out.println(name+" 运行了.."); 
					} 
				} 
			} 
		}else{
			while(true){ 
				synchronized (obj2) { 
					synchronized (obj1) { 
						System.out.println(name+" 运行了.."); 
					} 
				} 
			} 
		} 
	}
	public static void main(String[] args) { 
		Object obj1 = new Object(); 
		Object obj2 = new Object(); 
		Thread t1 = new ThreadDeadLock(obj1,obj2); 
		Thread t2 = new ThreadDeadLock(obj1,obj2); 
		t1.start(); 
		t2.start(); 
	} 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值