java程序天生就是多线程的,每一个程序的运行都包含多个线程的协作。多线程的好处在于1.可以充分利用计算机资源 2.使程序响应更加快速,但是任何事情都有其两面性。缺点也同样存在,1.多线程对计算机硬件(cpu,内存等)要求较高 2.计算机资源有限,太多的线程运行,会导致上下文频繁切换,甚至出现死锁等情况,出现非预期效果。
一.查看程序线程
查看简易Main程序线程
public class Main {
public static void main(String[] args) {
//获取最大线程对象
ThreadMXBean threadMXBean= ManagementFactory.getThreadMXBean();
//把线程放入集合
ThreadInfo[] threadInfos=threadMXBean.dumpAllThreads(false,false);
//循环输出线程
for (ThreadInfo threadInfo:threadInfos){
System.out.println(threadInfo);
}
}
}
输出
“Monitor Ctrl-Break” Id=6 RUNNABLE (in native)
“Attach Listener” Id=5 RUNNABLE
“Signal Dispatcher” Id=4 RUNNABLE
“Finalizer” Id=3 WAITING
“main” Id=1 RUNNABLE
上面即为当一个主程序运行时,里面运行的线程
二.线程的启动方式
利用实现runable和callable进行启动新线程
例子:利用runable和callable启动新线程(callable和runable的区别在于callable可以返回值)
public class ThreadTest {
//1.利用rubable进行线程的进行
public static class userunable implements Runnable{
//runable中的方法不能有返回值
@Override
public void run() {
System.out.println("runable is start");
}
}
//2.利用callable进行线程的执行
public static class usecallable implements Callable{
//callable可以返回Object类型的值
@Override
public String call() throws Exception {
System.out.println("callable is start");
return "sjw is good";
}
}
//调用线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
//启动rubable
userunable userunable=new userunable();
new Thread(userunable).start();
//启动callable
usecallable usecallable=new usecallable();
//callable需要利用jdk自带的类包装成runable进行运行
FutureTask<String> futureTask=new FutureTask<String>(usecallable);
//启动callable线程
new Thread(futureTask).start();
//获取并输出线程返回的内容
System.out.println(futureTask.get());
}
}
利用继承Thread实现新线程启动
例子:
public class ThreadTest {
private static class useThread extends Thread{
//使用Thread父类构造方法,传入自定义名称
//子类不能重写父类构造方法,因为子类不能继承父类,所以也就不存在重写的概念
public useThread (String ThreadName){
super(ThreadName);
}
public void run(){
System.out.println(Thread.currentThread().getName()+"is run");
}
}
public static void main(String[] args) {
useThread useThread=new useThread("MyThread");
useThread.start();
}
}
三.线程的终止方式
常见的线程终止有三种方式
1.自然的执行完 2.抛出异常 3.利用JDK提供的中断方法
在传统JDK提供的方法中,大多已经过时或者不建议使用,例如:stop(),resume(),suspend(),原因在于它们中断过程十分强硬粗暴,不论线程进行到何种程度,强行中断,暂停或挂起的方法执行后,也不在释放资源,可能会引起线程的死锁问题。
为了使线程安装的停止工作,引入了三种方法 interruput()、isinterrupted()、interrupted()。
方法 | 解释 |
---|---|
interruput() | 中断一个线程,其没有stop()那么强硬,像是总线程向要中断的线程进行打招呼,告诉其该中断了,线程此时依旧可以处理自己的任务,至于最后是否中断,由线程自行决定。只是把其中断位判断为true |
isiterrupted() | 判断当前线程是否处于中断状态,判断中断位是true还是false |
interupted() | static方法,判断线程是否处于中断状态,并且把中断位改为false |
Java中,线程的设计核心思想是线程的协作,而不是线程的抢占。
线程的中断
public class ThreadTest {
private static class useThread extends Thread{
//使用Thread父类构造方法,传入自定义名称
//子类不能重写父类构造方法,因为子类不能继承父类,所以也就不存在重写的概念
public useThread (String ThreadName){
super(ThreadName);
}
public void run(){
//判断是否中断,如果中断位为false,则执行
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is run");
}
System.out.println("hello thread");
}
}
public static void main(String[] args) throws InterruptedException {
useThread useThread=new useThread("MyThread");
useThread.start();
//线程睡眠20ms
Thread.sleep(20);
//睡眠过后进行线程的中断操作
useThread.interrupt();
}
}
输出
线程的中断与否,完全由线程自行决定,interrupt()方法只是把中断位标记为true
但是方法只要抛出InterruptedException异常,那么线程的中断位会复位成false,如果想继续保持中断状态,需要在catch中再次加入中断方法interrput() 。
例如:
public class ThreadTest {
private static class useThread extends Thread{
//使用Thread父类构造方法,传入自定义名称
//子类不能重写父类构造方法,因为子类不能继承父类,所以也就不存在重写的概念
public useThread (String ThreadName){
super(ThreadName);
}
public void run(){
//判断是否中断,如果中断位为false,则执行
while (!isInterrupted()) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
System.out.println(isInterrupted());
e.printStackTrace();
}
}
System.out.println("hello thread");
}
}
public static void main(String[] args) throws InterruptedException {
useThread useThread=new useThread("MyThread");
useThread.start();
//线程睡眠20ms
Thread.sleep(20);
//睡眠过后进行线程的中断操作
useThread.interrupt();
}
}
会输出
四.深入理解run()和start()
start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
run() : 就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!
public class ThreadTest {
private static class MyThread extends Thread{
public Thread2(String name){
super(name);
}
public void run(){
System.out.println(Thread.currentThread().getName()+" hello");
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread("MyThread");
System.out.println(Thread.currentThread().getName()+" is run");
myThread.run();
System.out.println(Thread.currentThread().getName()+" is start");
myThread.start();
}
}
五.线程的几种状态
cpu切换线程所用是线程轮番机制,当线程新建后,就进入了就绪状态,就绪状态是一个可被运行的状态,当线程轮番到它,即进行运行状态的切换,当此线程sleep()或者wait()即达到一个阻塞状态,阻塞状态并不代表此线程会消失,只是在等待,当线程轮番机制到它,依旧会运行。
注意:yield()方法,是从运行状态转换成可运行状态。yield处理后的线程与sleep和wait是不同的。sleep和wait处理后的线程如果没有到时间,线程轮转机制下一次是不会轮转到它的,但是yield中断后,当线程轮转机制轮转到它,立马会转成运行状态。
六.线程优先级的范围
线程优先级高的,分配的时间片就会多。线程的优先级分为1-10 缺省为5。数字越大代表优先级越高。利用线程.setPriority()进行设置,但是在实际开发中,即使设置了优先级,查看操作系统底层也可能都显示未设置。所以线程优先级更多是一种概念。
七.守护线程
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,
1、守护线程,比如垃圾回收线程,就是最典型的守护线程。
2、用户线程,就是应用程序里的自定义线程。
守护线程和主线程是同死的,主线程消失,守护线程也就随之消失,但是一定要注意守护线程的finally并不一定会执行。
八.线程synchroinzed
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。
在负责后台开发的时候,很多时候都是提供接口给前端开发人员去调用,会遇到这样的场景:
需要提供一个存钱接口,每个用户名在完成存成功前只能一次,我们可以将成功存钱的用户在数据库用个标记保存起来。如果这个用户再来存的时候,查询数据库看该用户是否存过。
但是问题来了,假设用户手速很快,极短时间内点了两次存钱按钮(前端没有进行控制,我们也不能依赖前端去控制)。那么可能存了两次钱(实际是一次),而且有可能第二次调用的时候查询数据库的时候,第一次存钱还没有执行完成更新余额标记。
这种场景就可以使用到synchronized
没有使用线程锁的时候
public class ThreadTest {
//创建一个线程,负责运行输入金额的方法
public static class ThreadOne extends Thread{
public void run(){
try {
inputMoney(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void inputMoney(int money) throws InterruptedException {
System.out.println("进入线程");
System.out.println("录入金额"+money);
Thread.sleep(500);
System.out.println("退出线程");
}
public static void main(String[] args) {
ThreadOne threadOne=new ThreadOne();
Thread thread1=new Thread(threadOne);
Thread thread2=new Thread(threadOne);
thread1.start();
thread2.start();
}
}
输出
加入同步后
public class ThreadTest {
//创建一个线程,负责运行输入金额的方法
public static class ThreadOne extends Thread{
public void run(){
try {
inputMoney(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static synchronized void inputMoney(int money) throws InterruptedException {
System.out.println("进入线程");
System.out.println("录入金额" + money);
Thread.sleep(500);
System.out.println("退出线程");
}
public static void main(String[] args) {
ThreadOne threadOne=new ThreadOne();
Thread thread1=new Thread(threadOne);
Thread thread2=new Thread(threadOne);
thread1.start();
thread2.start();
}
}
输出:
synchroinzed锁分为类锁和对象锁
对象锁
public synchroinzed void method(){
}
public void method(){
synchroinzed(this){
}
}
类锁:如果加在static上的锁称之为类锁
public static synchroinzed void method(){
}
其实,类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。
如下:调用静态方法必须用标准化(类名.静态方法),如果是实例化对象的调用法则无效。
public class SynchornizedTest {
public static synchronized void method1(){
for (int i =0;i<10;i++) {
System.out.println(Thread.currentThread().getName() + ": method1 is start" + i);
}
}
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
SynchornizedTest.method1();
}
},"t1");
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
SynchornizedTest.method1();
}
},"t2");
t1.start();
t2.start();
}
}
不管哪种锁,synchroinzed锁 锁的是对象,不是同一个对象,线程是可以同时运行的
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
五、以上规则对其它对象锁同样适用.
当线程获得对象锁访问synchronized代码块时,其他线程的依旧可以访问此对象的非synchronized代码块
public class ThreadTest {
public void method1() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}
public void method2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
public static void main(String[] args) {
final ThreadTest thread = new ThreadTest();
Thread t1 = new Thread( new Runnable() { public void run() { thread.method1(); } }, "t1" );
Thread t2 = new Thread( new Runnable() { public void run() { thread.method2(); } }, "t2" );
t1.start();
t2.start();
}
}
输出结果
当某个线程获得对象锁,线程进行某个同步代码块访问的时候,其它线程不能访问其他同步代码块
public class ThreadTest {
public void method1(){
synchronized (this) {
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public void method2(){
synchronized (this){
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public static void main(String[] args) {
final ThreadTest threadTest=new ThreadTest();
Thread thread1=new Thread(new Thread(new Runnable() {
@Override
public void run() {
threadTest.method1();
}
}),"t1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
threadTest.method2();
}
},"t2");
thread1.start();
thread2.start();
}
}
输出结果:
重要:synchronized本质上都是锁对象,都作用于同一个对象,当开发项目时,Spring一定要注意实例化对象,同步锁作用必须是同一个对象。
九.并发编程的三大特性
1.原子性
原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题.
2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.有序性
程序是按照顺序规则进行执行
程序顺序原则:一个线程内保证语义的串行性,比如第二条语句依赖第一条语句的结果,那么就不能先执行第二条再执行第一条。
volatile原则:volatile变量的写先于读,着保证了volatile变量的可见性
锁规则:先解锁,后续步骤再加锁。加锁不能重排到解锁之前,这样加锁行为无法获得锁(刚加上就解了)
传递性:A先于B,B先于C,那么A先于C
线程的start()先于它的每个动作
线程的所有操作先于线程的终结(可以通过Tread.join()方法结束、Thread.isAlive()的返回值判断一个线程是否终结)
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行、结束先于finalize()方法。
十.Volatile关键字
对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。
对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。
volatile修饰变量后该变量有这么一种效果:线程每一次读该变量都是直接从主存(JVM的主存)中读,而不是从线程的工作内存中;每一次写该变量都会同时写到主存中,而不仅仅是线程的工作内存中。
如下图:线程B,用SetA()方法,传入A=15值,然后主内存更改以前的值,将A改成15,当线程A调用getA()方法时,会首先消灭掉以前的值,并从主内存读取A值,将15存入线程内存中。
volatile线程是不安全的
例如:原先主内存A=10,B线程传输值的过程中,肯定要进行A=A+15的运算,但是运算过程是需要时间的,此时如果线程C又进行了A=25的赋值,那么主内存的值突然变成了25,那么此时运算过程就会发生不可控。所以线程是不安全的。
因为经过Volatile线程的只能保证读操作都是从主内存中读取数据,依旧无法保证写操作从工作内存写入主内存的时候没有其它线程抢先一步写入。
所以 volatile最好的适用场景是一个线程写,多个线程读
十一.ThreadLocal
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。ThreadLocal可以看作是一个Map的容器,它主要用于众多线程中,每个线程储存属于自我资源的数据。常常见于连接池,每个线程用来储存自己的连接信息。每个线程可以储存自己的重要信息,这样保证了,即使多个线程共享这些资源,但是也不会发生冲突,因为这些数据都被存在自己的ThreadLocal副本里。但是弊端在于会额外占用内存,所以建议储存一些小的数据。
set (value)设置当前线程的线程局部变量的值。
public Object get()该方法返回当前线程所对应的线程局部变量。
public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
因为ThreadLoacl本质上是一个对象,储存在堆上,所以保存的副本也在堆中。
十二.等待和通知的标准范式
线程轮巡机制虽然很常见,但是有一定的弊端,例如:米饭15分钟后焖好,但是没有提醒,那么你需要多次去查询是否到15分钟,查询的过程很浪费资源,并且容易处理不及时。所以线程协作中更多的是生产者和消费者。也可以理解为通知方和等待方
等待方:
(1)获取对象的锁
(2)循环判断条件是否与自己相符合,不符合则调用wait(),继续等待
(3)条件满足,则执行业务逻辑
通知方:
(1)获取对象的锁
(2)改变条件
(3)通知所有在等待的线程
十三.notify()和notifyAll()的区别
notify()随机唤醒一个线程(或根据线程的优先级),理论上将,因为所有等待线程都会进入队列,所以理论上唤醒第一个。notifyAll()唤起所有wait()阻塞的线程。
设计一个可以使线程wait()的类
public class Express {
public final static String CITY="shanghai";
public int KM;//运输快递历程
public String site; //运输地点
public Express(int KM,String site){
this.KM=KM;
this.site=site;
}
/*变化公里数,然后通知处于wait()状态并需要处理公里数的线程进行业务处理*/
public synchronized void changeKm(){
this.KM=101;
notifyAll();
}
/*变化发货地点,然后通知处于wait()状态并需要处理地点的线程进行业务处理*/
public synchronized void changeSite(){
this.site="beijing";
notify();
}
public synchronized void waitKm(){
while (this.KM<=100){
try {
wait();
System.out.println("changeKm Thread :"+
Thread.currentThread().getName()+"is be notify");
}catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the km is:"+this.KM+"I will change KM for db");
}
public synchronized void waitSite(){
while ("shanghai".equals(this.site)){
try {
wait();
System.out.println("changeSite Thread:"+
Thread.currentThread().getName()+"is be notify");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the site is:"+this.site+"I will change site for db");
}
}
实验notify()和notifyAll()的区别
/**
* 测试notify(),notifyAll()
*/
public class TestCheck {
//实例化类,并初始化值
public static Express express=new Express(0,Express.CITY);
/*检查当前KM的线程,如果不符合条件则会一直等待*/
public static class checkCurrentKm extends Thread{
public void run(){
express.waitKm();
}
}
/*检查当前的地点的线程,如果不符合条件则会一直等待*/
public static class checkCurrentSite extends Thread{
public void run(){
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
//启动三个检查线程
for (int i=0;i<3;i++){
new checkCurrentKm().start();
}
for (int i=0;i<3;i++){
new checkCurrentSite().start();
}
Thread.sleep(1000);
express.changeSite();//本质上也就是使用其notify()
// express.changeKm();//本质上也就是使用其notifyAll();
}
}
当notify()时:输出
当notifyAll()时:输出
notify()是随机对一个线程做唤醒处理
notifyAll()是对所有等待的线程进行唤醒,如果唤醒后依旧不符合条件,那么将会继续等待。
在实际开发中,应尽可能选择notifyAll(),因为notify()存在唤醒的不确定性(易被想唤醒的线程外其他线程拦截信号)。
十四.等待超时模式范式
等待超时模式常常用于线程池连接。
//首先会计算下超时时间
long overtime=开始时间now+等待持续时间T
//保持时间
long remain=等待持续时间T
while(result不符合条件&&remain>0 还在持续){
sleep(剩下等待时间t);
剩下等待的时间t=overtiome超时时间-now当前时间;
}
//如果线程被唤醒
return result;
实现一个数据库连接池:
import java.sql.Connection;
import java.util.LinkedList;
import java.util.List;
public class DBpool {
//数据连接池用LinkedList是为了增加和删除迅速
public static LinkedList<Connection> connectionPool=new LinkedList<Connection>();
/*首先设置一个初始化连接数,根据配置初始化连接数进行连接*/
public void initConnection(int initNumber){
if (initNumber>0) {
for (int i = 0; i < initNumber; i++) {
//将新创建的连接从链表尾部插入新创建的连接
connectionPool.addLast();
}
}
}
/*定义取连接的类,以及最大等待时间*/
public Connection getConn(long mills) throws InterruptedException {
//给连接池对象上一个对象锁,同时只允许一个线程对其操作
synchronized (connectionPool) {
//设置等待时间为负数
if (mills<0){
//为空则表示无法给予连接,所以连接池等待
while(connectionPool.isEmpty()){
connectionPool.wait();
}
//不为空则给出头连接
return connectionPool.removeFirst();
}else {
//定义超时时间
long overtime = System.currentTimeMillis() + mills;
long remain = mills;
//当连接池中依旧无连接,并且等待时间还存在的时候,如果由连接空出来就需退出此while循环
while (connectionPool.isEmpty() && remain > 0) {
//使线程休眠剩余等待时间
connectionPool.wait(remain);
//不断更新还剩等待时间
remain=overtime-System.currentTimeMillis();
}
//上一个循环的退出表示要不不为空,要不是超时了,那么此时第一种情况是有连接的所以还需要拿一下
Connection connection=null;
if (!connectionPool.isEmpty()){
connection=connectionPool.removeFirst();
}
//连接有即返回连接,无连接则返回空
return connection;
}
}
}
/*释放数据库连接(本质上是把连接再次放回链表)*/
public void releaseConn(Connection conn){
if (conn!=null){
synchronized (connectionPool) {
connectionPool.addLast(conn);
//唤醒所有等待线程,告诉他们新连接来了,让他们重新去抢,最后抢不到又会被等待
connectionPool.notifyAll();
}
}
}
}
十五.join()
有一个线程A和一个线程B,如何保证,线程A执行完了以后线程B就执行?
join()!
和countDownLatch()
那么Join()方法的作用是什么呢?
线程A执行了B线程的join()方法,那么线程A必须等线程B执行完成后,线程A才能执行自己的方法。像一种操作,插队!
public class TestJoin {
public static class JumpQueue implements Runnable{
public Thread thread;
//传入要join(),要插队的线程
public JumpQueue(Thread thread){
this.thread=thread;
}
@Override
public void run() {
try {
//让线程进行插队
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" is run");
}
public static void main(String[] args) {
//main主线程
Thread previous=Thread.currentThread();
for (int i=0;i<10;i++){
//将主线程加入0线程,0加入1线程...
Thread thread=new Thread(new JumpQueue(previous),String.valueOf(i));
System.out.println(previous.getName()+" is jump before the thread "+
thread.getName());
thread.start();
//进行一个线程一个线程的替换,0换1,1换2..
previous=thread;
}
System.out.println(Thread.currentThread().getName()+"is run");
}
}
}
输出结果
main is jump before the thread 0
0 is jump before the thread 1
1 is jump before the thread 2
2 is jump before the thread 3
3 is jump before the thread 4
4 is jump before the thread 5
5 is jump before the thread 6
6 is jump before the thread 7
7 is jump before the thread 8
8 is jump before the thread 9
main
0 is run
1 is run
2 is run
3 is run
4 is run
5 is run
6 is run
7 is run
8 is run
9 is run
证明,join的本质上类似于插队,线程1让线程2插了,线程2又让线程3进行插队,那么只有线程2和线程3执行完,线程1才可以继续执行。
十六.notify()、notifyAll()、sleep()、yield()对锁产生的影响
线程yiled()方法执行后,线程持有的锁是不释放的。
线程sleep()方法执行后,线程持有的锁是不释放的。
在调用wait()方法必须要持有锁,因为没有锁是无法达到执行的。在执行wait()方法后,持有的锁会被释放。当wait()方法返回的时候,依旧会重新持有锁。
notify()和notifyAll()在使用前必须要持有锁,因为你无锁唤醒别人不能给别人锁是没有意义的。并且notify()一般在锁区域代码的最后一段,因为notify()释放锁必须保证后方代码执行完才会释放,不然会执行完后方业务代码,才能进行锁的释放。
public sychnonized void change(){
this.Km=Km;
notify();
//只有当本句业务代码执行完成后,才会继续往后执行
this.change=change;
}