学习多线程
聊到多线程,我们一定会联想到进程,那么它们两有啥关系呢??
1、区分线程和进程
1.1、 进程
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
下面的任务管理器就有进程的例子一
1.2、线程
线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。换句话说,就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程。包括这两个串行和并行,搞清楚这个,我们才能更好地理解多线程。
一个正在运行的软件(如迅雷)就是一个进程,一个进程可以同时运行多个任务( 迅雷软件可以同时下载多个文件,每个下载任务就是一个线程), 可以简单的认为进程是线程的集合。
线程是一条可以执行的路径。
- 对于单核CPU而言:多线程就是一个CPU在来回的切换,在交替执行。(串行)
- 对于多核CPU而言:多线程就是同时有多条执行路径在同时(并行)执行,每个核执行一个线程,多个核就有可能是一块同时执行的。(并发)
举一个比较好理解的例子:
我们打开上面任务管理器的火绒安全软件,这个火绒就是一个进程,那么什么是线程呢?线程就是下面软件里的一系列功能!!
一个进程至少有一个线程,所以呢!至少可以开启一个功能比如先开启病毒查杀。分为多线程、单线程
- 单线程:多线程就是我必须一个一个功能开启,相当于排队,执行完一个再执行下一个。
- 多线程:我可以同时进行几个任务。清理垃圾的时候,还可以进行查杀病毒、电脑加速等等其他的操作,没有先后顺序。
1.3、两者关系
-
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
-
资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。
-
处理机分给线程,即真正在处理机上运行的是线程。
-
线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
1.4、多线程的优点
多线程可以提高程序的效率。
实际生活案例:村长要求喜洋洋在一个小时内打100桶水,可以喜洋洋一个小时只能打25桶水,如果这样就需要4个小时才能完成任务,为了在一个小时能够完成,喜洋洋就请美洋洋、懒洋洋、沸洋洋,来帮忙,这样4只羊同时干活,在一小时内完成了任务。原本用4个小时完成的任务现在只需要1个小时就完成了,如果把每只羊看做一个线程,多只羊即多线程可以提高程序的效率。
接下来就是我们需要重点掌握的如何创建线程了!!!!
2、创建多线程
在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口.(其实准确来讲,应该有三种,还有一种是实现Callable接口)
对了,我们知道了多线程这概念,应该不知道什么是多线程吧,先了解一下什么是多线程吧!!
Integer count = 0;
public void getCount() {
count ++;
System.out.println(count);
}
输出为:
出现2个26,为什么会出现这种情况呢,原因就是我开启了3个线程,全部开启出现重复是因为有2个线程同时进入了这个输出的方法中,都打印的数据,这就是多线程,但是这是不安全的线程,那需要怎么才能解决呢?后面会告诉你们如果解决线程安全。
现在先带大家了解3种创建多线程的方法咯!
2.1、Thread类
这里继承Thread类的方法是比较常用的一种,如果说你只是想起一条线程。没有什么其它特殊的要求,那么可以使用Thread。但是我们项目中Runable创建会更方便!!
- 继承Thread类的实例
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
}
}
/**
输出:
A运行 : 0
B运行 : 0
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4
**/
start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
我们可以看看源码:
Thread类
package java.lang;
public class Thread implements Runnable {
// 构造方法
public Thread(Runnable target);
public Thread(Runnable target, String name);
public synchronized void start();
}
Runnable接口
package java.lang;
@FunctionalInterface
public interface Runnable {
pubic abstract void run();
}
这么就可以看出Thread类是先变成Runnable接口的然后通过接口执行run方法。
接下来我们可以看看Runnable接口的实现创建多线程!
2.2、Runnable接口
public class Main {
public static void main(String[] args) {
// 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}
可以看到两种方式都是围绕着Thread和Runnable,继承Thread类把run()写到类中,实现Runnable接口是把run()方法写到接口中然后再用Thread类来包装, 两种方式最终都是调用Thread类的start()方法来启动线程的。
无论是继承Thread类还是实现Runnable接口,都是需要涉及到两种方法。
- 继承Thread类:直接将线程类注入Thread类中,然后再Runnable类实现
可形态化
,调用类中的start开启线程。 - 实现Runnable接口:先将该类
可形态化
然后将该类注入Thread类中调用start开启线程。
其实还可以通过匿名实现
public class Main {
public static void main(String[] args) {
// 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}).start();
// 尾部代码块, 是对匿名内部类形式的语法糖
new Thread() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}.start();
// Runnable是函数式接口,所以可以使用Lamda表达式形式
Runnable runnable = () -> {System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());};
new Thread(runnable).start();
}
}
还有最后一种方法实现
2.3、实现Callable接口
实现Callable接口,重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread
特点:有返回值的线程,能取消线程,可以判断线程是否执行完毕
public class Main {
public static void main(String[] args) throws Exception {
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// get方法会阻塞调用的线程
Integer sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
return sum;
}
}
FutureTask
public class FutureTask<V> implements RunnableFuture<V> {
// 构造函数
public FutureTask(Callable<V> callable);
// 取消线程
public boolean cancel(boolean mayInterruptIfRunning);
// 判断线程
public boolean isDone();
// 获取线程执行结果
public V get() throws InterruptedException, ExecutionException;
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
三种方式比较:
- Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
- Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
- Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
- 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
- Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现
3、线程的周期
线程的状态包括5种
- 创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
- 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
- 运行(running)状态: 执行run()方法\
- 阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用:
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
- 死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)
4、线程调度
线程调度也就是我们在线程中常调用的方法。
- 调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
- 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
- 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
- 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
- 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
- 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
线程Thread类常用的方法:
Thread类
public class Thread implements Runnable {
// 线程名字
private volatile String name;
// 线程优先级(1~10)
private int priority;
// 守护线程
private boolean daemon = false;
// 线程id
private long tid;
// 线程组
private ThreadGroup group;
// 预定义3个优先级
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
// 构造函数
public Thread();
public Thread(String name);
public Thread(Runnable target);
public Thread(Runnable target, String name);
// 线程组
public Thread(ThreadGroup group, Runnable target);
// 返回当前正在执行线程对象的引用
public static native Thread currentThread();
// 启动一个新线程
public synchronized void start();
// 线程的方法体,和启动线程没毛关系
public void run();
// 让线程睡眠一会,由活跃状态改为挂起状态
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException;
// 打断线程 中断线程 用于停止线程
// 调用该方法时并不需要获取Thread实例的锁。无论何时,任何线程都可以调用其它线程的interruptf方法
public void interrupt();
public boolean isInterrupted()
// 线程是否处于活动状态
public final native boolean isAlive();
// 交出CPU的使用权,从运行状态改为挂起状态
public static native void yield();
public final void join() throws InterruptedException
public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos) throws InterruptedException
// 设置线程优先级
public final void setPriority(int newPriority);
// 设置是否守护线程
public final void setDaemon(boolean on);
// 线程id
public long getId() { return this.tid; }
// 线程状态
public enum State {
// new 创建
NEW,
// runnable 就绪
RUNNABLE,
// blocked 阻塞
BLOCKED,
// waiting 等待
WAITING,
// timed_waiting
TIMED_WAITING,
// terminated 结束
TERMINATED;
}
}
public static void main(String[] args) {
// main方法就是一个主线程
// 获取当前正在运行的线程
Thread thread = Thread.currentThread();
// 线程名字
String name = thread.getName();
// 线程id
long id = thread.getId();
// 线程优先级
int priority = thread.getPriority();
// 是否存活
boolean alive = thread.isAlive();
// 是否守护线程
boolean daemon = thread.isDaemon();
// Thread[name=main, id=1 ,priority=5 ,alive=true ,daemon=false]
System.out.println("Thread[name=" + name + ", id=" + id + " ,priority=" + priority + " ,alive=" + alive + " ,daemon=" + daemon + "]");
}
我们在上面创建多线程的方法中都使用了start()方法和run()方法,到底是什么作用呢?
1、写一个类继承自Thread类,重写run方法。用start方法启动线程
2、写一个类实现Runnable接口,实现run方法。用new Thread(Runnable target).start()方法来启动
相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start是排队!等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。
4.1、start()
start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,
这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的,
这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
4.2、run()
run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。
5、常用方法
5.1、sleep()与interrupt()
public static native void sleep(long millis) throws InterruptedException;
public void interrupt();
什么是sleep()方法呢!!
sleep(long millis): 睡眠指定时间,程序暂停运行,睡眠期间会让出CPU的执行权,去执行其它线程,同时CPU也会监视睡眠的时间,一旦睡眠时间到就会立刻执行(因为睡眠过程中仍然保留着锁,有锁只要睡眠时间到就能立刻执行。但是由于sleep方法不会释放锁对象,所以在一个同步代码块中调用这个方法后,线程虽然休眠了,但其他线程无法访问它的锁对象。所以也不会释放CPU,所谓占着茅坑不拉屎!!!。
- sleep(): 睡眠指定时间,即让程序暂停指定时间运行,时间到了会继续执行代码,如果时间未到就要醒需要使用interrupt()来随时唤醒
interrupt
(): 唤醒正在睡眠的程序,调用interrupt()方法,会使得sleep()方法抛出InterruptedException
异常,当sleep()方法抛出异常就中断了sleep的方法,从而让程序继续运行下去
举个例子:
public static void main(String[] args) throws Exception {
Thread thread0 = new Thread(()-> {
try {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t太困了,让我睡10秒,中间有事叫我,zZZ。。。");
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t被叫醒了,又要继续干活了");
}
});
thread0.start();
// 这里睡眠只是为了保证先让上面的那个线程先执行
Thread.sleep(2000);
new Thread(()-> {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t醒醒,醒醒,别睡了,起来干活了!!!");
// 无需获取锁就可以调用interrupt
thread0.interrupt();
}).start();
}
/**
输出:
Sun Nov 14 22:42:02 GMT+08:00 2021 Thread-0 太困了,让我睡10秒,中间有事叫我,zZZ。。。
Sun Nov 14 22:42:04 GMT+08:00 2021 Thread-1 醒醒,醒醒,别睡了,起来干活了!!!
Sun Nov 14 22:42:04 GMT+08:00 2021 Thread-0 被叫醒了,又要继续干活了
**/
5.2、wait()与notify()
wait、notify和notifyAll方法是Object类的final native方法。所以这些方法不能被子类重写,Object类是所有类的超类,因此在程序中可以通过this或者super来调用this.wait(), super.wait(),
区别:
wait()方法和sleep()方法不一样,调用wait方法的线程,不会自己唤醒,需要线程调用 notify / notifyAll 方法唤醒等待池中的所有线程,才会进入就绪队列中等待系统分配资源。sleep方法会自动唤醒,如果时间不到,想要唤醒,可以使用interrupt方法强行打断。
- wait(): 导致线程进入等待阻塞状态,会一直等待直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。wait(long timeout): 时间到了自动执行,类似于sleep(long millis)
- notify(): 该方法只能在同步方法或同步块内部调用, 随机选择一个(注意:只会通知一个)在该对象上调用wait方法的线程,解除其阻塞状态
- notifyAll(): 唤醒所有的wait对象
但是不会按优先级竞争资源
当想要调用wait( )进行线程等待时,必须要取得这个锁对象的控制权(对象监视器),一般是放到synchronized(obj)代码中。
注意:
-
假设有三个线程执行了obj.wait( ),那么obj.notifyAll( )则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
-
当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行。
来个实例吧:
public class WaitNotifyTest {
// 在多线程间共享的对象上使用wait
private String[] shareObj = { "true" };
public static void main(String[] args) {
WaitNotifyTest test = new WaitNotifyTest();
ThreadWait threadWait1 = test.new ThreadWait("wait thread1");
threadWait1.setPriority(2);
ThreadWait threadWait2 = test.new ThreadWait("wait thread2");
threadWait2.setPriority(3);
ThreadWait threadWait3 = test.new ThreadWait("wait thread3");
threadWait3.setPriority(4);
ThreadNotify threadNotify = test.new ThreadNotify("notify thread");
threadNotify.start();
threadWait1.start();
threadWait2.start();
threadWait3.start();
}
class ThreadWait extends Thread {
public ThreadWait(String name){
super(name);
}
public void run() {
synchronized (shareObj) {
while ("true".equals(shareObj[0])) {
System.out.println("线程"+ this.getName() + "开始等待");
long startTime = System.currentTimeMillis();
try {
shareObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("线程" + this.getName()
+ "等待时间为:" + (endTime - startTime));
}
}
System.out.println("线程" + getName() + "等待结束");
}
}
class ThreadNotify extends Thread {
public ThreadNotify(String name){
super(name);
}
public void run() {
try {
// 给等待线程等待时间
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (shareObj) {
System.out.println("线程" + this.getName() + "开始准备通知");
shareObj[0] = "false";
shareObj.notifyAll();
System.out.println("线程" + this.getName() + "通知结束");
}
System.out.println("线程" + this.getName() + "运行结束");
}
}
}
/**
线程wait thread1开始等待
线程wait thread3开始等待
线程wait thread2开始等待
线程notify thread开始准备通知
线程notify thread通知结束
线程notify thread运行结束
线程wait thread2等待时间为:2998
线程wait thread2等待结束
线程wait thread3等待时间为:2998
线程wait thread3等待结束
线程wait thread1等待时间为:3000
线程wait thread1等待结束
**/
5.4、wait()与interrupt()方法
wait()方法的作用是释放锁,加入到等待队列,当调用interrupt()方法后,线程必须先获取到锁后,然后才抛出异常InterruptedException 。注意: 在获取锁之前是不会抛出异常的,只有在获取锁之后才会抛异常
所有能抛出InterruptedException的方法都可以通过interrupt()来取消的
- sleep()
- wait()
- join()
-
notify/notifyAll是java.lang.Object类的方法,唤醒的是该实例的等待队列中的线程,而不能直接指定某个具体的线程。notify/notifyAll唤醒的线程会继续执行wait的下一条语句,另外执行notify/notifyAll时线程必须要获取实例的锁
-
interrupte方法是java.lang.Thread类的方法,可以直接指定线程并唤醒,当被interrupt的线程处于sleep或者wait中时会抛出InterruptedException异常。执行interrupt()并不需要获取取消线程的锁。
-
总之notify/notifyAll和interrupt的区别在于是否能直接让某个指定的线程唤醒、执行唤醒是否需要锁、方法属于的类不同
来看看wait()方法是怎么执行的
public class SleepWaitTest {
public static void main(String[] args) throws InterruptedException {
SleepWaitTest object = new SleepWaitTest();
new Thread(() -> {
synchronized (object) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
try {
object.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
}
}).start();
// 先上面的线程先执行
Thread.sleep(1000);
new Thread(() -> {
synchronized (object) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
}
}
}).start();
}
}
/**
Sun Nov 14 23:19:14 GMT+08:00 2021 Thread-0 等待打印文件...
Sun Nov 14 23:19:15 GMT+08:00 2021 Thread-1 0
Sun Nov 14 23:19:15 GMT+08:00 2021 Thread-1 1
Sun Nov 14 23:19:15 GMT+08:00 2021 Thread-1 2
Sun Nov 14 23:19:15 GMT+08:00 2021 Thread-1 3
Sun Nov 14 23:19:15 GMT+08:00 2021 Thread-1 4
Sun Nov 14 23:19:19 GMT+08:00 2021 Thread-0 打印结束。。。
**/
分析:
因main方法中有Thread.sleep(1000)所以上面的线程Thread-0肯定会被先执行,当Thread-0被执行时就拿到了object对象锁,然后进入wait(5000)5秒钟等待,此时wait释放了锁,然后Thread-1就拿到了锁就执行线程体,Thread-1执行完后就释放了锁,当等待5秒后Thread-0就能再次获取object锁,这样就继续执行后面的代码。wait方法是释放锁的,如果wait方法不释放锁那么Thread-1是拿不到锁也就没有执行的机会的,事实是Thread-1得到了执行,所以说wait方法会释放锁
结论:打印结果显示,当线程使用wait()方法时会将当前CPU释放,待下一个线程执行完被wait的线程被唤醒则和剩下的线程竞争资源。意思是wait()方法会释放锁,不然下一个线程获取不到锁就不能执行。
sleep与wait的区别:
- sleep在Thread类中,wait在Object类中
- sleep不会释放锁,wait会释放锁
- sleep使用interrupt()来唤醒,wait需要notify或者notifyAll来通知
5.5、interrupt()与interrupted()
两者的区别:
- interrupt()是用来设置中断状态的。返回true说明中断状态被设置了而不是被清除了。我们调用sleep、wait等此类可中断(throw InterruptedException)方法时,一旦方法抛出InterruptedException,当前调用该方法的线程的中断状态就会被jvm自动清除了,就是说我们调用该线程的isInterrupted 方法时是返回false。如果你想保持中断状态,可以再次调用interrupt方法设置中断状态。这样做的原因是,java的中断并不是真正的中断线程,而只设置标志位(中断位)来通知用户。如果你捕获到中断异常,说明当前线程已经被中断,不需要继续保持中断位。
- interrupted是静态方法,返回的是当前线程的中断状态。例如,如果当前线程被中断(没有抛出中断异常,否则中断状态就会被清除),你调用interrupted方法,第一次会返回true。然后,当前线程的中断状态被方法内部清除了。第二次调用时就会返回false。如果你刚开始一直调用isInterrupted,则会一直返回true,除非中间线程的中断状态被其他操作清除了。
5.6、join()
让当前线程加入父线程,加入后父线程会一直wait,直到子线程执行完毕后父线程才能执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
将某个线程加入到当前线程中来,一般某个线程和当前线程依赖关系比较强,必须先等待某个线程执行完毕才能执行当前线程。一般在run()方法内使用
简单的讲就是父线程(threa1)和子线程(thread2)执行,父线程会比子线程先结束线程,但是父线程又需要获取到子线程的结果,所以就需要thread1等待thread2咯,就给thread1wait()一下,让thread2先执行完成再结束父线程。
带大家看看join的源码
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 循环检查线程的状态是否还活着,如果死了就结束了,如果活着继续等到死
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
public final synchronized void join(long millis, int nanos) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
再来看看一个例子更加理解
public class JoinTest {
public static void main(String[] args) {
new Thread(new ParentRunnable()).start();
}
}
class ParentRunnable implements Runnable {
@Override
public void run() {
// 线程处于new状态
Thread childThread = new Thread(new ChildRunable());
// 线程处于runnable就绪状态
childThread.start();
try {
// 当调用join时,parent会等待child执行完毕后再继续运行
// 将某个线程加入到当前线程
childThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "父线程 running");
}
}
}
class ChildRunable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "子线程 running");
}
}
}
/**
Sun Nov 14 23:42:35 GMT+08:00 2021 Thread-1子线程 running
Sun Nov 14 23:42:36 GMT+08:00 2021 Thread-1子线程 running
Sun Nov 14 23:42:37 GMT+08:00 2021 Thread-1子线程 running
Sun Nov 14 23:42:38 GMT+08:00 2021 Thread-1子线程 running
Sun Nov 14 23:42:39 GMT+08:00 2021 Thread-1子线程 running
Sun Nov 14 23:42:39 GMT+08:00 2021 Thread-0父线程 running
Sun Nov 14 23:42:39 GMT+08:00 2021 Thread-0父线程 running
Sun Nov 14 23:42:39 GMT+08:00 2021 Thread-0父线程 running
Sun Nov 14 23:42:39 GMT+08:00 2021 Thread-0父线程 running
Sun Nov 14 23:42:39 GMT+08:00 2021 Thread-0父线程 running
**/
这里也可以下面在父线程执行过程中的时候join方法也可以,看实际需求。
5.7、yield()
交出CPU的执行时间,不会释放锁,让线程进入就绪状态,等待重新获取CPU执行时间,yield就像一个好人似的,当CPU轮到它了,它却说我先不急,先给其他线程执行吧, 此方法很少被使用到,
public static void main(String[] args) {
new Thread(new Runnable() {
int sum = 0;
@Override
public void run() {
long beginTime=System.currentTimeMillis();
for (int i = 0; i < 99999; i++) {
sum += 1;
// 去掉该行执行用2毫秒,加上271毫秒
Thread.yield();
}
long endTime=System.currentTimeMillis();
System.out.println("用时:"+ (endTime - beginTime) + " 毫秒!");
}
}).start();
}
- sleep(long millis): 需要指定具体睡眠的时间,不会释放锁,睡眠期间CPU会执行其它线程,睡眠时间到会立刻执行
- yeid(): 交出CPU的执行权,不会释放锁,和sleep不同的时当再次获取到CPU的执行,不能确定是什么时候,而sleep是能确定什么时候再次执行。两者的区别就是sleep后再次执行的时间能确定,而yeid是不能确定的
- yield会把CPU的执行权交出去,所以可以用yield来控制线程的执行速度,当一个线程执行的比较快,此时想让它执行的稍微慢一些可以使用该方法,想让线程变慢可以使用sleep和wait,但是这两个方法都需要指定具体时间,而yield不需要指定具体时间,让CPU决定什么时候能再次被执行,当放弃到下次再次被执行的中间时间就是间歇等待的时间
5.8、 setDaemon(boolean on)
- 用户线程:如果主线程main停止掉,不会影响用户线程,用户线程可以继续运行。setDaemon(false);默认
- 守护线程:如果主线程死亡,守护线程如果没有执行完毕也要跟着一块死(就像皇上死了,带刀侍卫也要一块死),GC垃圾回收线程就是守护线程 setDaemon(true);
6、线程安全
上面介绍了多线程及一些方法的使用,接下来就通过一个经典的售票系统来实现线程安全的介绍!!
卖票案例需求分析
某天某个景区售票,门票票只有100张,景区有三个售票窗口,同时售票,共享票源(100张)
通过多线程的方式实现三个售票窗口同时售票
首先要明确票源只能有一个
其次需要创建三个Thread的对象去执行卖票的方法,卖完票后要将剩余的票数返回给统一的票源;
代码实现:
//票源
public class Ticket implements Runnable {
private int tickets = 100; //票源
TicketOffice ticketOffice = new TicketOffice();
@Override
public void run() {
while(true){
this.tickets = ticketOffice.sellTicket(tickets); //sellTicket由synchronized修饰
//返回的tickets重新传给成员变量this.tickets以保证票源的一致
}
}
}
2、售票规则
public class TicketOffice {
public synchronized int sellTicket(int tickets) {
if (tickets > 0) {
try {
Thread.sleep(0); //模拟CPU忙碌的停顿
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread th = Thread.currentThread();
System.out.println(th.getName() + " 线程数为:" + Thread.activeCount() + " 出售第:" + tickets--);
}
return tickets;
}
}
在案例中我把售票窗口代码写道了另一个类中,并设置为了静态方法,可以直接通过类名来调用
3.测试类代码
public class ThreadDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket(); //票源固定100张
Thread thread0 = new Thread(ticket); //创建新线程
thread0.start();
Thread thread1 = new Thread(ticket);
thread1.start();
Thread thread2 = new Thread(ticket);
thread2.start();
}
输出:
出现重复的票号,说明了说明,当我们去取票的时候,几个窗口再同时卖一张票,导致出现重票
!!! 显然输出结果有问题,我们设置的票源总共有5张,而此时三个窗口总和加起来卖了15张!
出现原因:
没有正确使用synchronized方法,启动线程的时候每个线程获取到了一个sellTicket方法并且都拿到了总数一样的票数,在进行锁方法,并没有达到效果。this.tickets = ticketOffice.sellTicket(tickets);外部也加上synchronized代码块,从而使得共享成员不会错误赋值。
代码修改:
public class Ticket implements Runnable {
private int tickets = 100;
TicketOffice ticketOffice = new TicketOffice();
@Override
public void run() {
while(true){
synchronized (this) {
this.tickets = ticketOffice.sellTicket(tickets); //sellTicket由synchronized修饰
}
}
}
}
这里使用了同步代码块
synchronized (this) {
this.tickets = ticketOffice.sellTicket(tickets); //sellTicket由synchronized修饰
}
实现了该方法调用的锁。
那肯定有人有疑问?this是代表什么呢!!
这里的同步锁分为对象锁和类锁,首先呢,同步方法是典型的对象锁,指this。
代码块使用的this也是对象锁,指当前对象的锁,所以当使用同一个类的对象,就会产生那么这两个线程都需要获得该对象锁,一个获得后另一个线程必须等待,
public class TestSynchronized {
public synchronized void minus() {
int count = 5;
for (int i = 0; i < 5; i++) {
count--;
System.out.println(Thread.currentThread().getName() + " - " + count);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
}
public class Run {
public static void main(String[] args) {
final TestSynchronized test = new TestSynchronized();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
test.minus();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
test.minus();
}
});
thread1.start();
thread2.start();
}
}
先输出线程0再输出线程1,可以看到,结果是交替的,说明线程是交替执行的,说明如果某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象,当某个线程获得对象的锁之后,只能阻止其他线程获得同一个锁。
类锁:
类锁对应的关键字是static sychronized,是一个全局锁,无论多少个对象否共享同一个锁(也可以锁定在该类的class上或者是classloader对象上),同样是保障同一个时刻多个线程同时访问同一个synchronized块,当一个线程在访问时,其他的线程等待。
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(4);
Account suo = new Account();
Account suo1 = new Account();
for (int i=0;i<4;i++){
int n=i+1;
int j=n*3;
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000*(new Random().nextInt(8)));
System.out.println("线程"+n+"准备好了");
barrier.await();
if(n==1||n==3)
suo.add(j);//1,3
else
suo.mul(j);//2,4
} catch (Exception e) {
e.printStackTrace();
}
}
},"线程"+i).start();
}
public class Account {
/**
* 账户类
*/
private static int count = 100;
public static synchronized void add(int m){
String name = Thread.currentThread().getName();
System.out.println("类锁添加" + m + "钱," + name + "添加后:" + (count+=m));
}
public static synchronized void mul(int m){
String name = Thread.currentThread().getName();
System.out.println("类锁减少" + m + "钱," + name + "消费后:" + (count-=m));
}
}
是多个线程去访问一个类里面的方法
类锁是对静态方法使用synchronized关键字后,无论是多线程访问单个对象还是多个对象的sychronized块,都是同步的。
上面还使用了同步方法
public synchronized int sellTicket(int tickets) {
}
都是实现线程安全的方法。
还有一种方法就是实现Lock锁机制,采用lock()加锁,unlock()解锁,来保护指定代码块
// Lock锁机制
Lock lock = new ReentrantLock();
while(tickets > 0) {
try {
lock.lock();
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票");
tickets--;
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}finally {
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName()+"--->售票结束!");
}