admin 管理员组文章数量: 887016
目录
一、多线程介绍
1.1 多线程中的基本概念
1.1.1多线程与进程
1.1.2 进程、线程的区别和联系
1.1.3 并发和并行的区别
1.1.4 线程的执行特点
1.1.5 主线程与子线程
二、线程的创建及生命周期
2.1 通过继承Thread类实现多线程
2.1.1 继承Thread类实现多线程的步骤:
2.2 通过Runnable接口实现多线程
2.3 线程的执行流程
2.4 线程状态和生命周期
三、线程的使用
3.1 终止线程的典型方式
3.2 线程休眠
3.3 线程让步
3.4 线程联合
3.4.1 线程联合案例
3.5 Thread类中的其他常用方法
3.5.1 获取当前线程名称
3.5.2 修改线程名称
3.5.3判断线程是否存活
四、线程的优先级
4.1 线程优先级的使用
五、守护线程
5.1 守护线程的使用
六、线程同步
6.1 什么是线程冲突?
6.2 同步问题的提出
6.3 实现线程同步
6.4 线程冲突案例演示
6.4.1 没有实现线程冲突
6.4.2 实现线程同步
6.5 使用this作为线程对象锁
6.6 使用字符串作为线程对象锁
6.7 使用Class作为线程对象锁
6.8 使用自定义对象作为线程对象锁
6.9 线程死锁
6.9.1 死锁的概念
6.9.2 解决线程死锁
七、线程并发协作
7.1 生产者消费者模式介绍
7.2 实现消费者与生产者模式
一、多线程介绍
1.1 多线程中的基本概念
1.1.1多线程与进程
什么是程序?
程序(Program)是一个静态的概念,一般对应于操作系统中的一个可执行文件。
什么是进程?
执行中的程序叫做进程(Process),是一个动态的概念。其实进程就是一个在内存中独立运行的程序空间 。进程之间相互独立数据不共享,都有自己的CPU时间。缺点是CPU负担较重而且浪费资源。
现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统,啥叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。如下,任务管理器中每个应用实际上就是个进程,还有很多的进程在后台运行。
什么是线程?
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
有些进程还不止同时干一件事,。我们可以将微信看作是一个进程,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。也就是说线程是在应用软件中相互独立,可以同时运行的功能。
为什么要有多线程?
多线程可以减少cpu在程序执行中的等待时间,提高CPU的执行效率。
多线程的应用场景
软件中的耗时操作、拷贝迁移大文件、加载大量的资源文件、聊天软件、后台服务器等都需要多线程技术,因为如果使用单线程的话加载这些资源会很耗时间,浪费了cpu。只要是想让多个事情同时运行就需要多线程。
1.1.2 进程、线程的区别和联系
小案例:
乔布斯想开工厂生产手机,费劲力气,制作一条生产线,这个生产线上有很多的器件以及材料。一条生产线就是一个进程。
只有生产线是不够的,所以找五个工人来进行生产,这个工人能够利用这些材料最终一步步的将手机做出来,这五个工人就是五个线程,为了提高生产率,有两种办法:
- 一条生产线上多招些工人,一起来做手机,这样效率是成倍増长,即单进程多线程方式。
- 多条生产线,每个生产线上多个工人,即多进程多线程
结论:
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
1.1.3 并发和并行的区别
并发是指在一段时间内同时做多个事情。当有多个线程在运行时,如果只有一个CPU,这种情况下计算机操作系统会采用并发技术实现并发运行,具体做法是采用“ 时间片轮询算法”,在一个时间段的线程代码运行时,其它线程处于就绪状。这种方式我们称之为并发。(Concurrent)。
并行指的是在同一时刻,有多个指令在多个CPU上同时执行。
结论:
- 串行(serial):一个CPU上,按顺序完成多个任务。
- 并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的。处理这些任务的CPU不止有一个,而是多个CPU处理不同的任务。
- 并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多于cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
1.1.4 线程的执行特点
方法的执行特点
Java程序中方法是串行执行的。
线程的执行特点
1.1.5 主线程与子线程
主线程
虚拟机程序实际上就是一个进程,当Java程序启动时,一个线程会立刻运行,该线程通常叫做程序的主线程(main thread),即main方法对应的线程,它是程序开始时就执行的。
Java应用程序会有一个main方法,是作为某个类的方法出现的。当程序启动时,该方法就会第一个自动的得到执行,并成为程序的主线程。也就是说,main方法是一个应用的入口,也代表了这个应用的主线程。JVM在执行main方法时,main方法会进入到栈内存,JVM会通过操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程。
主线程的特点
它是产生其他子线程的线程。
它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。
主线程只有一个,除了主线程其他都是子线程。
子线程
在主线程中创建并启动的线程,一般称之为子线程。
二、线程的创建及生命周期
2.1 通过继承Thread类实现多线程
2.1.1 继承Thread类实现多线程的步骤:
1、在Java中负责实现线程功能的类是java.lang.Thread 类。
此种方式的缺点:如果我们的类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。因为Java不允许多继承。
2、可以通过创建 Thread的实例来创建新的线程。
3、每个线程都是通过某个特定的Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。
4、通过调用Thread类的start()方法来启动一个线程。
线程的执行需要在实现Thread类中的run();方法,该方法实际上就是线程体,此外在启动线程时,不是调用run方法,而是调用Thread类中的start()方法类启动线程。
package cn.it.bz.Thread;
public class TestThread extends Thread {
//线程方法(线程体),当线程启动后该方法会立即执行。该方法不能直接调用,而是通过
// Thread类中的start();方法执行。
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称
}
}
//main方法就是线程中的主线程
public static void main(String[] args) {
TestThread testThread1 = new TestThread(); //创建子线程对象1
testThread1.start(); //启动线程,此时主线程和子线程1都在执行
TestThread testThread2 = new TestThread(); //创建子线程对象2
testThread2.start(); //启动线程,此时主线程和子线程1、2都在执行
}
}
2.2 通过Runnable接口实现多线程
在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了继承Thread类的缺点,即在实现Runnable接口的同时还可以继承某个类。两种方式比较看,实现Runnable接口的方式要通用一些。
从源码角度看,Thread类也是实现了Runnable接口。Runnable接口的源码如下:
public class Thread implements Runnable{……}
package cn.it.bz.Thread;
public class TestThread2 implements Runnable {
//线程方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//Thread类是java.lang包下的类不需要导包
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
//主线程
public static void main(String[] args) {
//表示将任务交给线程处理。new TestThread2对象可以看作是线程要执行的任务。
Thread thread1 = new Thread(new TestThread2());
thread1.start(); //启动线程
Thread thread2 = new Thread(new TestThread2());
thread2.start();
}
}
或者是使用Lambda表达式创建线程:
package cn.it.bz.Lambda;
//Runnable接口中只有一个抽象方法run,也就是说Runnable是个函数接口。
public class Test3 {
public static void main(String[] args) {
System.out.println("主线程"+ Thread.currentThread().getName()+"启动!");
//Lambda表达式实现run 方法。
Runnable runnable = () -> {
for (int i = 0; i < 10; i++ ) {
System.out.println(Thread.currentThread().getName() + ", "+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
};
//线程包装
Thread thread = new Thread(runnable, "Lambda线程");
//线程启动
thread.start();
System.out.println("主线程"+ Thread.currentThread().getName()+"结束!");
}
}
一个线程不能被启动两次。
2.3 线程的执行流程
线程被执行后先进入就绪态,等待被CPU执行。CPU通过时间片轮询的方式执行线程,当时间片用完后该线程又变为就绪态在就绪队列中等待CPU执行,而此时的CPU就处理其他线程。当线程出现故障时,无论该线程的时间片是否结束,cpu都会将该线程变为阻塞态放在阻塞队列中,阻塞结束的时候变为就绪态回到就绪队列中。当线程执行完毕之后线程进入死亡状态。
需要注意的是:就绪态不能直接变为阻塞态,阻塞态不能直接变为运行态。
2.4 线程状态和生命周期
一个线程对象在它的生命周期内,需要经历5个状态。
-
新生状态(New)
用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
-
就绪状态(Runnable)
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4种原因会导致线程进入就绪状态:
- 新建线程:调用start()方法,进入就绪状态;
- 阻塞线程:阻塞解除,进入就绪状态;
- 运行线程:调用yield()方法,直接进入就绪状态;
- 运行线程:JVM将CPU资源从本线程切换到其他线程。
3、运行状态(Running)
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
4、阻塞状态(Blocked)
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
有4种原因会导致阻塞:
- 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
- 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
- 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
- join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
5、死亡状态(Terminated)
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。当一个线程进入死亡状态以后,就不能再回到其它状态了。
三、线程的使用
3.1 终止线程的典型方式
终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。因为线程可能还有后续工作,不能直接将他们嘎了。控制子线程生死的是主线程。
package cn.it.bz.Thread;
import java.io.IOException;
public class KillThread implements Runnable {
//生死牌,true为生,false为死
private boolean flag = true;
//控制生死牌的方法
public void killThread(){
this.flag = false;
}
//子线程
@Override
public void run() {
System.out.println("子线程开始:"+Thread.currentThread().getName());
int i = 0;
while (flag){
System.out.println(Thread.currentThread().getName()+"-"+i);
i++;
try {
Thread.sleep(1000); //休眠,线程由运行状态变为阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("子线程结束");
}
//主线程
public static void main(String[] args) throws IOException {
System.out.println("主线程开始");
KillThread kill = new KillThread();
Thread thread = new Thread(kill);
//启动子线程
thread.start();
//使主线程阻塞
System.in.read();
//主线程结束,结束子线程
kill.killThread();
System.out.println("主线程结束");
}
}
主线程启动后,将创建的线程对象包装为Thread 对象,调用start();方法启动子线程。此时子线程执行while循环,主线程阻塞,但是子线程一直在执行,当从键盘输入数据时,主线程不再阻塞并开始向下执行,子线程杀死程序和打印输出语句,主线程是不会等待子线程死亡的。子线程被杀死时不是立即结束工作,而是先执行完线程(也就是run方法)后死亡。
3.2 线程休眠
sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。sleep方法为Thread类的静态方法,参数为休眠的毫秒数(1秒 = 1000毫秒)。
package cn.it.bz.Thread;
public class SleepThread implements Runnable {
@Override
public void run() {
System.out.println("子线程开始:"+Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
//子线程休眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//主线程,主线程在哪个类是没有区别
public static void main(String[] args) {
System.out.println("主线程开始");
SleepThread sleepThread = new SleepThread();
Thread thread = new Thread(sleepThread);
//启动子线程
thread.start();
System.out.println("主线程结束");
}
}
主线程是不会等待子线程的,两个线程分别执行各自的。
3.3 线程让步
yield()让当前正在运行的线程回到就绪状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
使用yield方法时要注意的几点:
- yield是一个静态的方法。
- 调用yield后,yield告诉当前线程把运行机会交给具有相同优先级的线程。
- yield不能保证,当前线程迅速从运行状态切换到就绪状态。
- yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。当让步线程遇到堵塞时先变为阻塞态,阻塞结束了再变为就绪态。
package cn.it.bz.Thread;
public class TestyieldThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 15; i++) {
//如果当前线程名字是Thread-1,就让步,而且只让步第一次
if ("Thread-1".equals(Thread.currentThread().getName())){
if (i == 0){
System.out.println("我™直接让步~");
Thread.yield();
}
}
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new TestyieldThread());//子线程1
Thread thread2 = new Thread(new TestyieldThread());//子线程2
//启动线程,线程的运行顺序取决于CPU的线程调度
thread1.start();
thread2.start();
}
}
3.4 线程联合
当前线程邀请调用方法的线程优先执行,在调用方法的线程执行结束之前,当前线程不能再次执行。线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。和Java中方法的执行顺序差不多。
join方法的使用
join()方法就是指调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,即将两个线程合并,用于实现同步控制。
package cn.it.bz.Thread;
import java.util.stream.Stream;
//子线程A
class A implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("当前A线程:"+Thread.currentThread().getName()+"--"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//主线程
public class TestJoinThread {
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new A());
threadA.start();
for (int i = 0; i < 10; i++) {
if (i == 2) {
//主线程联合A线程,直接在主线程调用join方法
threadA.join();
}
System.out.println("主线程:"+Thread.currentThread().getName()+"--"+i);
Thread.sleep(1000);
}
}
}
主线程和A线程在没有联合之前是同步执行的,但是执行到threadA.join();时,主线程会等待A线程执行完毕之后再执行。
3.4.1 线程联合案例
package cn.it.bz.Thread;
//儿子买烟线程
class SonThread implements Runnable{
@Override
public void run() {
System.out.println("儿子得知要去买烟,买烟需要十分钟");
for (int i = 0; i < 10; i++) {
System.out.println("第"+i+"分钟");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("儿子买烟回来了");
}
}
//爸爸抽烟线程
class FatherThread implements Runnable{
@Override
public void run() {
System.out.println("爸爸想抽烟发现烟抽完了,让儿子去买包华子");
//启动儿子买烟线程
Thread thread = new Thread(new SonThread());
thread.start();
//爸爸需要等着儿子买烟回来
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("儿子买烟出异常了,爸爸出门找儿子");
System.exit(1); //结束运行在虚拟机的进程,找儿子去吧
}
System.out.println("爸爸开心地接过烟,猛吸了一口,说真好!b( ̄▽ ̄)d ");
}
}
public class TestJoinDemo {
public static void main(String[] args) {
System.out.println("这是个爸爸和儿子的故事~");
Thread thread = new Thread(new FatherThread());
thread.start();
}
}
3.5 Thread类中的其他常用方法
3.5.1 获取当前线程名称
方式一
this.getName()获取线程名称,该方法适用于继承Thread实现多线程方式。
class GetName1 extends Thread{
@Override
public void run() {
System.out.println(this.getName());
}
}
方式二
Thread.currentThread().getName()获取线程名称,该方法适用于实现Runnable接口实现多线程方式。Thread.currentThread()获取当前线程对象
class GetName2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
3.5.2 修改线程名称
方式一
当线程继承Thread类时通过构造方法设置线程名称。
package cn.it.bz.Thread;
class SetName1 extends Thread{
//接受自己定义的线程名称
public SetName1(String name){
super(name); //调用父类的构造方法
}
@Override
public void run() {
System.out.println("SetName1线程名称:"+this.getName());
}
}
//主线程
public class TestSetNameThread {
public static void main(String[] args) {
SetName1 setName1 = new SetName1("setName1");
setName1.start();
}
}
方式二
当线程实现Runable接口时通过setName()方法设置线程名称。
package cn.it.bz.Thread;
class SetName implements Runnable{
@Override
public void run() {
System.out.println("当前线程名字:"+Thread.currentThread().getName());
}
}
public class TestSetNameThread2 {
public static void main(String[] args) {
//创建Thread对象
Thread thread = new Thread(new SetName());
thread.setName("😄");
thread.start();
}
}
3.5.3判断线程是否存活
isAlive()方法: 判断当前的线程是否处于活动状态。返回值是true表示活着,false表示死亡。
活动状态是指线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就认为线程是存活的。
package cn.it.bz.Thread;
class Alive implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("当前线程:"+Thread.currentThread().getName()+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//主线程
public class TestAliveThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Alive());
thread.setName("🤭");
thread.start();
System.out.println("当前🤭线程是否存活:"+thread.isAlive());//true
Thread.sleep(7000); //主线程休眠7秒
System.out.println("当前🤭线程是否存活:"+thread.isAlive());//false
}
}
四、线程的优先级
什么是线程的优先级
每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示,范围从1到10,一个线程的默认优先级是5。
Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
注意
线程的优先级,不是说哪个线程优先执行,如果设置某个线程的优先级高。那就是有可能被执行的概率高。并不是绝对优先执行。
4.1 线程优先级的使用
使用下列方法获得或设置线程对象的优先级。
- int getPriority(); //获取当前进程的优先级
- void setPriority(int newPriority); //设置进程的优先级
注意:线程优先级是在线程启动之前就分配的,优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。
package cn.it.bz.Thread;
class Priority implements Runnable{
private int num = 0;//记录当前线程执行次数
private boolean flag = true; //生死牌
@Override
public void run() {
while (flag){
System.out.println("当前线程名称:"+Thread.currentThread().getName()+",执行次数是:"+num++);
}
}
public void stop(){
flag = false;
}
}
public class PriorityThread {
public static void main(String[] args) throws InterruptedException {
Priority priority1 = new Priority();
Thread thread1 = new Thread(priority1,"线程1");
Priority priority2 = new Priority();
Thread thread2 = new Thread(priority2,"线程2");
System.out.println("线程1的优先级:"+thread1.getPriority());//5
System.out.println("线程2的优先级:"+thread2.getPriority());//5
thread1.setPriority(Thread.MAX_PRIORITY);// thread1.setPriority(10);
thread2.setPriority(Thread.MIN_PRIORITY);
//启动线程
thread1.start();
thread2.start();
//主线程休眠
Thread.sleep(2000);
//结束线程
priority1.stop();
priority2.stop();
}
}
五、守护线程
守护线程(即Daemon Thread),是一个服务线程,准确地来说就是服务其他的线程,这是它的作用,而其他的线程只有一种,那就是用户线程。
在Java中有两类线程:
- User Thread(用户线程):就是应用程序里的自定义线程。
- Daemon Thread(守护线程):比如垃圾回收线程,就是最典型的守护线程。
守护线程特点:守护线程会随着用户线程死亡而死亡。
守护线程与用户线程的区别:
用户线程,不随着主线程的死亡而死亡。用户线程只有两种情况会死掉,1、在run中异常终止。2、正常把run执行完毕,线程死亡。
守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡。
5.1 守护线程的使用
package cn.it.bz.Thread;
//守护线程
class Daemon implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("守护线程:"+Thread.currentThread().getName()+","+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//主线程
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
//实例化守护线程
Thread thread = new Thread(new Daemon(),"啊哈哈");
thread.setDaemon(true); //将普通线程变为守护线程
thread.start(); //守护线程启动
Thread.sleep(3000);
System.out.println("主线程结束");//主线程结束,守护线程结束。守护线程不一定非得守护主线程。
}
}
六、线程同步
6.1 什么是线程冲突?
如图,该进程中的线程一在对进程空间中的对象进行修改时,突然时间片用完。此时线程一只是修改了对象的name,age并未做出修改。线程二读取到的对象的值就是错误的。
6.2 同步问题的提出
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。 比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。
6.3 实现线程同步
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。这套机制就是synchronized关键字。
synchronized语法结构:
synchronized(对象锁){
同步代码
}
synchronized关键字使用时需要考虑的问题:
- 需要对那部分的代码在执行时具有线程互斥(线程同步)的能力(线程互斥:并行变串行)。
- 需要对哪些线程中的代码具有互斥能力(通过synchronized锁对象来决定)。
- 拥有相同对象锁的线程才会做线程互斥。
synchronized两种用法:
synchronized 方法
通过在方法声明中加入 synchronized关键字来声明,语法如下:
public synchronized void accessVal(int newVal);
synchronized 在方法声明时使用:放在访问控制符(public)之前或之后。这时同一个对象下synchronized方法在多线程中执行时,该方法是同步的,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。
synchronized块
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。
6.4 线程冲突案例演示
我们以银行取款经典案例来演示线程冲突现象。
银行取钱的基本流程基本上可以分为如下几个步骤。
(1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。
(2)用户输入取款金额
(3)系统判断账户余额是否大于或等于取款金额
(4)如果余额大于或等于取款金额,则取钱成功;如果余额小于取款金额,则取钱失败。
6.4.1 没有实现线程冲突
package cn.it.bz.Thread;
//账户类
class Account{
private String password; //账户密码
private double balance; //账户余额
public Account() {
}
public Account(String password, double balance) {
this.password = password;
this.balance = balance;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
//取款线程
class DrawMoneyThread implements Runnable{
//账户对象
private Account account;
//取款金额
private double drawMoney;
public DrawMoneyThread() {
}
public DrawMoneyThread(Account account, double drawMoney) {
this.account = account;
this.drawMoney = drawMoney;
}
//取款线程体
@Override
public void run() {
//判断当前账户余额>=取款金额
if (this.account.getBalance()>this.drawMoney){
System.out.println(Thread.currentThread().getName()+"取款成功!"+"余额:"+(this.account.getBalance()-this.drawMoney));
//线程休眠,账户余额修改
try {
Thread.sleep(1000);
this.account.setBalance(this.account.getBalance()-this.drawMoney);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println(Thread.currentThread().getName()+"取款失败!!余额不足");
}
}
}
public class TestDrawMoneyThread {
public static void main(String[] args) {
Account account = new Account("12345",2000);
Thread manThread = new Thread(new DrawMoneyThread(account, 1300),"男人取款线程");//男人取款线程
Thread womanThread = new Thread(new DrawMoneyThread(account, 1000),"女人取款线程");//女人取款线程
manThread.start();
womanThread.start();
}
}
6.4.2 实现线程同步
package cn.it.bz.Thread;
//账户类
class Account{
private String password; //账户密码
private double balance; //账户余额
public Account() {
}
public Account(String password, double balance) {
this.password = password;
this.balance = balance;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
//取款线程
class DrawMoneyThread implements Runnable{
//账户对象
private Account account;
//取款金额
private double drawMoney;
public DrawMoneyThread() {
}
public DrawMoneyThread(Account account, double drawMoney) {
this.account = account;
this.drawMoney = drawMoney;
}
//取款线程体
@Override
public void run() {
//判断当前账户余额>=取款金额
//同步范围,synchronized不能加在run方法上,因为不起作用。
synchronized (this.account){ //锁住账户对象,this指的是当前DrawMoneyThread线程对象
if (this.account.getBalance()>this.drawMoney){
System.out.println(Thread.currentThread().getName()+"取款成功!"+"余额:"+(this.account.getBalance()-this.drawMoney));
//线程休眠,账户余额修改
try {
Thread.sleep(1000);
this.account.setBalance(this.account.getBalance()-this.drawMoney);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println(Thread.currentThread().getName()+"取款失败!!余额不足");
}
}
}
}
public class TestDrawMoneyThread {
public static void main(String[] args) {
Account account = new Account("12345",2000);
Thread manThread = new Thread(new DrawMoneyThread(account, 1300),"男人取款线程");//男人取款线程
Thread womanThread = new Thread(new DrawMoneyThread(account, 1000),"女人取款线程");//女人取款线程
manThread.start();
womanThread.start();
}
}
6.5 使用this作为线程对象锁
对象锁的作用是决定了哪些线程具有互斥的效果,其类型必须是对象类型不能是基本数据类型。
语法结构:
synchronized(this){
//同步代码
}
或
public synchronized void accessVal(int newVal){
//同步代码
}
package cn.it.bz.Thread;
//程序员类
class Programmer{
private String name;
public Programmer(String name) {
this.name = name;
}
//打开电脑
public void openPC() throws InterruptedException {
synchronized (this){ //this代表的是张三
System.out.println(this.name+"接通电源");
Thread.sleep(1000);//哪个线程执行该方法,哪个线程就会休眠
System.out.println(this.name+"按开机按键");
Thread.sleep(1000);
System.out.println("系统启动中……");
Thread.sleep(1000);
System.out.println("系统启动成功!");
}
}
//编码
public void coding() throws InterruptedException {
synchronized (this){ //this代表的是张三
System.out.println(this.name+"双击IDEA程序");
Thread.sleep(1000);
System.out.println("IDEA程序启动成功");
Thread.sleep(1000);
System.out.println("开开心心写代码");
}
}
}
//打开电脑的工作线程
class OpenPC implements Runnable{
private Programmer programmer;
public OpenPC(Programmer programmer) {
this.programmer = programmer;
}
@Override
public void run() {
try {
this.programmer.openPC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//编写代码的工作线程
class Coding implements Runnable{
private Programmer programmer;
public Coding(Programmer programmer) {
this.programmer = programmer;
}
@Override
public void run() {
try {
this.programmer.coding();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//主线程
public class TestSyncThread {
public static void main(String[] args) {
Programmer zs = new Programmer("张三");
OpenPC openPC = new OpenPC(zs) ;
Thread threadOpenPC = new Thread(openPC);
threadOpenPC.start();
Coding coding = new Coding(zs) ;
Thread threadCoding= new Thread(coding);
threadCoding.start();
}
}
6.6 使用字符串作为线程对象锁
因为字符串是引用类型的数据而且字符串是个常量不会改变,因此所有线程都会同步。
语法结构:
synchronized(“字符串”){
//同步代码
}
package cn.it.bz.Thread;
//程序员类
class Programmer{
private String name;
public Programmer(String name) {
this.name = name;
}
//去卫生间
public void wc() throws InterruptedException {
synchronized ("随便写"){ //this代表的是张三
System.out.println(this.name+"打开卫生间门");
Thread.sleep(1000);
System.out.println(this.name+"上厕所");
Thread.sleep(1000);
System.out.println(this.name+"嘘嘘~");
Thread.sleep(1000);
System.out.println(this.name+"离开厕所");
}
}
}
//去卫生间的线程
class WC implements Runnable{
private Programmer programmer;
public WC(Programmer programmer) {
this.programmer = programmer;
}
@Override
public void run() {
try {
this.programmer.wc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//主线程
public class TestSyncThread {
public static void main(String[] args) {
Programmer zs = new Programmer("张三");
Programmer ls = new Programmer("李四");
WC wc1 = new WC(zs) ;
Thread threadZSWC= new Thread(wc1);
threadZSWC.start();
WC wc2 = new WC(ls) ;
Thread threadLSWC= new Thread(wc2);
threadLSWC.start();
}
}
6.7 使用Class作为线程对象锁
语法结构:
synchronized(XX.class){
//同步代码
}
或
synchronized public static void accessVal()
package cn.it.bz.Thread;
//销售类
class Sale{
private String name;
public Sale(String name) {
this.name = name;
}
//领奖金
public void money() throws InterruptedException {
synchronized (Sale.class){
System.out.println(this.name+"被领导表扬");
Thread.sleep(1000);
System.out.println(this.name+"拿钱");
Thread.sleep(1000);
System.out.println(this.name+"表示感谢");
Thread.sleep(1000);
System.out.println(this.name+"开开心心拿钱走人,O(∩_∩)O");
}
}
}
//程序员类
class Programmer{
private String name;
public Programmer(String name) {
this.name = name;
}
//领取奖金
public void money() throws InterruptedException {
synchronized (Programmer.class){
System.out.println(this.name+"被领导表扬");
Thread.sleep(1000);
System.out.println(this.name+"拿钱");
Thread.sleep(1000);
System.out.println(this.name+"表示感谢");
Thread.sleep(1000);
System.out.println(this.name+"开开心心拿钱走人,O(∩_∩)O");
}
}
}
//程序员领取奖金的线程
class GetMoney implements Runnable{
private Programmer programmer;
public GetMoney(Programmer programmer) {
this.programmer = programmer;
}
@Override
public void run() {
try {
this.programmer.money();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//销售领取奖金的线程
class SaleGetMoney implements Runnable{
private Sale sale;
public SaleGetMoney(Sale sale) {
this.sale = sale;
}
@Override
public void run() {
try {
this.sale.money();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//主线程
public class TestSyncThread {
public static void main(String[] args) {
Programmer zs = new Programmer("张三");
Programmer ls = new Programmer("李四");
new Thread(new GetMoney(zs)).start(); //张三领取奖金
new Thread(new GetMoney(ls)).start();
Sale sale1 = new Sale("小红");
Sale sale2 = new Sale("小兰");
new Thread(new SaleGetMoney(sale1)).start();
new Thread(new SaleGetMoney(sale2)).start();
}
}
相同部门的员工在拿钱时是串行的,不同部门之间是并行的。
6.8 使用自定义对象作为线程对象锁
语法结构:
synchronized(自定义对象){
//同步代码
}
package cn.it.bz.Thread;
//经理类
class Manager{
private String name;
public Manager(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//敬酒的方法
public void cheers(String Mname,String Ename) throws InterruptedException {
synchronized (this){ //manager充当对象锁
System.out.println(Mname+"经理走到"+Ename+"面前");
Thread.sleep(1000);
System.out.println(Ename+"拿起酒杯");
Thread.sleep(1000);
System.out.println(Mname+"经理和"+Ename+"干杯");
}
}
}
//敬酒的线程
class Cheers implements Runnable{
private Manager manager; //经理对象
private String Ename;
public Cheers(Manager manager, String ename) {
this.manager = manager;
Ename = ename;
}
@Override
public void run() {
try {
/* synchronized (this.manager){ //或者在线程类中使用经理对象锁
this.manager.cheers(manager.getName(),Ename);
}*/
this.manager.cheers(manager.getName(),Ename);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//主线程
public class TestSyncThread {
public static void main(String[] args) {
Manager manager = new Manager("张三");
Cheers cheers1 = new Cheers(manager,"李四");
Thread thread1 = new Thread(cheers1);
thread1.start();
Cheers cheers2 = new Cheers(manager,"王五");
Thread thread2 = new Thread(cheers2);
thread2.start();
}
}
6.9 线程死锁
6.9.1 死锁的概念
“死锁”指的是:
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。
某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。比如,“化妆线程”需要同时拥有“镜子对象”、“口红对象”才能运行同步块。那么,实际运行时,“小丫的化妆线程”拥有了“镜子对象”,“大丫的化妆线程”拥有了“口红对象”,都在互相等待对方释放资源,才能化妆。这样,两个线程就形成了互相等待,无法继续运行的“死锁状态”。
package cn.it.bz.Thread;
//口红类
class Lipstick{
}
//镜子类
class Mirror{
}
//化妆线程
class MakeUp implements Runnable{
private int flag; //flag = 0表示拿到是口红,否则是镜子
private String name; //化妆人的名字
static Lipstick lipstick = new Lipstick(); //唯一的口红
static Mirror mirror = new Mirror(); //唯一的镜子
public MakeUp(int flag,String name){
this.flag = flag;
this.name = name;
}
//开始化妆
public void doMakeUp() throws InterruptedException {
if (this.flag == 0){ //拿到的是口红
synchronized (lipstick){ //口红只有一份,因此会产生线程互斥
System.out.println(name+"拿着口红");
Thread.sleep(1000);
synchronized (mirror){
System.out.println(name+"拿着镜子");
}
}
}else {
synchronized (mirror){
System.out.println(name+"拿着镜子");
Thread.sleep(1000);
synchronized (lipstick){
System.out.println(name+"拿着口红");
}
}
}
}
@Override
public void run() {
try {
doMakeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class DeadLockThread {
public static void main(String[] args) {
MakeUp makeUp1 = new MakeUp(0,"大丫");
Thread thread_makeUp1 = new Thread(makeUp1);
thread_makeUp1.start();
MakeUp makeUp2 = new MakeUp(1,"小丫");
Thread thread_makeUp2 = new Thread(makeUp2);
thread_makeUp2.start();
}
}
6.9.2 解决线程死锁
死锁是由于 “同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁,也就是synchronized中不能嵌套synchronized。
package cn.it.bz.Thread;
//口红类
class Lipstick{
}
//镜子类
class Mirror{
}
//化妆线程
class MakeUp implements Runnable{
private int flag; //flag = 0表示拿到是口红,否则是镜子
private String name; //化妆人的名字
static Lipstick lipstick = new Lipstick(); //唯一的口红
static Mirror mirror = new Mirror(); //唯一的镜子
public MakeUp(int flag,String name){
this.flag = flag;
this.name = name;
}
//开始化妆
public void doMakeUp() throws InterruptedException {
if (this.flag == 0){ //拿到的是口红
synchronized (lipstick){ //口红只有一份,因此会产生线程互斥
System.out.println(name+"拿着口红");
Thread.sleep(1000);
}
synchronized (mirror){
System.out.println(name+"拿着镜子");
}
}else {
synchronized (mirror){
System.out.println(name+"拿着镜子");
Thread.sleep(1000);
}
synchronized (lipstick){
System.out.println(name+"拿着口红");
}
}
}
@Override
public void run() {
try {
doMakeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class DeadLockThread {
public static void main(String[] args) {
MakeUp makeUp1 = new MakeUp(0,"大丫");
Thread thread_makeUp1 = new Thread(makeUp1);
thread_makeUp1.start();
MakeUp makeUp2 = new MakeUp(1,"小丫");
Thread thread_makeUp2 = new Thread(makeUp2);
thread_makeUp2.start();
}
}
七、线程并发协作
7.1 生产者消费者模式介绍
多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
角色介绍
-
什么是生产者?
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
-
什么是消费者?
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
-
什么是缓冲区?
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
缓冲区是实现并发的核心,缓冲区的设置有两个好处:
-
实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离,解除了生产者与消费者之间的耦合。
-
解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。
7.2 实现消费者与生产者模式
package cn.it.bz.Thread;
//大饼类
class DaBing{
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
//数据缓冲区,放大饼的,不需要实现线程接口,缓冲区不是线程。
class SyncStack{
//定义存放大饼的盒子
private DaBing[] daBings = new DaBing[10];
//定义操作盒子的索引
private int index;
//放大饼
synchronized public void setDaBing(DaBing daBing) throws InterruptedException {
//先判断盒子是否满了
while (daBings.length == index){
//满了不生产了让线程等待
this.wait(); //wait是Object类的方法,该方法必须在synchronized块中调用,wait执行后线程会将持有的对象锁释放并进入阻塞状态。-
}
//让消费者来取大饼
this.notify();
this.daBings[index] = daBing;
index++; //注意
}
//取大饼
synchronized public DaBing getDaBing() throws InterruptedException {
//盒子有饼才能取
while (index == 0){
//等待
this.wait();
}
this.notify();//该方法必须在synchronized块中调用,唤醒一个线程
this.index--;
return daBings[index];
}
}
//生产者线程
class ShengChan implements Runnable{
//生产者生产的大饼需要放到缓冲区
private SyncStack syncStack;
public ShengChan(SyncStack syncStack) {
this.syncStack = syncStack;
}
@Override
public void run() {
//生产大饼
for (int i = 0; i < 9; i++) {
DaBing daBing = new DaBing();
daBing.setId(i);
System.out.println("生产大饼"+daBing.getId());
try {
syncStack.setDaBing(daBing); //放到缓冲区
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者线程
class XiaoFei implements Runnable{
//消费者到缓冲区拿
private SyncStack syncStack;
public XiaoFei(SyncStack syncStack) {
this.syncStack = syncStack;
}
@Override
public void run() {
//取大饼
for (int i = 0; i < 9; i++) {
DaBing daBing = new DaBing();
try {
DaBing daBing1 = syncStack.getDaBing();//拿大饼
System.out.println("取大饼"+daBing1.getId());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//主线程
public class TestProduceThread {
public static void main(String[] args) {
//创建缓冲区对象
SyncStack syncStack = new SyncStack();
//生产者线程
Thread thread_shengChan = new Thread(new ShengChan(syncStack));
//消费者线程
Thread thread_xiaofei = new Thread(new XiaoFei(syncStack));
thread_shengChan.start();
thread_xiaofei.start();
}
}
wait方法就好比张三和李四合租房子。张三去厨房做饭了,然后将厨房的门锁上了,好巧不巧张三此时心脏病犯了,张三马上晕倒了,于是张三把厨房的门打开了,让李四进去了。如果进行对象锁的释放的话就相当于是厨房的门永远锁着,李四永远进不了厨房。
版权声明:本文标题:JAVA基础 多线程技术(V1.0) 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726361376h944711.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论