Java并发(线程状态、线程调度、线程同步)
线程状态
线程共有5种状态,在特定情况下,线程可以在不同的状态之间切换。
-
5种具体状态
- 创建状态:实例化一个新的线程对象,还未启动。
- 就绪状态:创建好的线程对象调用start方法完成启动,进入线程池等待抢占CPU资源。
- 运行状态:线程对象获取了CPU资源,在一定的时间内执行任务。
- 阻塞状态:正在运行的线程暂停执行任务,释放所占用的CPU资源,并在解除阻塞状态之后也不能直接回到运行状态,而是重新回到就绪状态,等待获取CPU资源。
- 终止状态:线程运行完毕或因为异常导致该线程终止运行。
-
线程状态的转换
线程调度
-
线程休眠
让当前线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其他线程的调度方式,通过sleep()来实现。sleep(long millis),调用时需要传入休眠时间,单位为豪秒。
public static native void sleep(long millis) throws InterruptedException;
sleep是静态本地方法,可以通过类调用,也可以通过对象调用,方法定义抛出InterruptedException异常,lnterruptedException继承Exception,外部调用时必须手动处理异常。
在类中调用sleep方法
public class MyRunnable extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { if (i==5){ //单位是毫秒 所以5000毫秒即5秒 同时sleep需要处理抛出的异常 try { sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i+"-----MyRunnable"); } } } }
也可以在类的外部调用sleep方法
public static void main(String[] args) throws InterruptedException { MyThread myThread = new MyThread(); try { myThread.sleep(5000); }catch (InterruptedException e){ e.printStackTrace(); } myThread.start(); }
在外部调用需要注意,休眠一定要放在启动之前。否则如果启动后马上休眠不一定能够休眠成功。
也可以通过这种方法来使主线程(main)休眠,即直接通过静态方式调用sleep方法。
如果线程采用的是实现Runnable接口的方式,那么没办法直接使用sleep()方法。因为sleep()是Thread类里 的方法,所以其可以直接用。而Runnable只是接口,其里面并没有sleep()方法,只能采用Thread.sleep() 这种方法。
-
线程合并
合并是指将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中的两个自线程顺序执行。通过调用join方法来合并,具体操作如下。
线程甲和线程乙,线程甲执行到某个时间点的时候调用线程乙的join方法,则表示从当前时间点开始CPU资源被线程乙独占(即乙.jion()后便到乙线程执行了),线程甲进入阻塞状态,直到线程乙执行完毕,线程甲进入就绪状态,等待获取CPU资源进入运行状态。
join方法重载,join()表示乙线程执行完毕之后才能执行其他线程,join(long millis)表示乙线程执行millis 毫秒之后,无论是否执行完毕,其他线程都可以和它争夺CPU 资源。
public final void join() throws InterruptedException { join(0); }
public final synchronized void join(long millis) throws InterruptedException { //源代码太长 在这里就不贴出来了 }
使用方法如下
public class test { public static void main(String[] args) throws InterruptedException { MyThread myThread = new MyThread(); myThread.start(); try { for (int i = 0; i < 5000; i++) { //在i=1500时候main线程合并了MyThread线程,此时切换为MyThread线程进行 if (i==1500){ myThread.join(); } System.out.println(i+"----main"); } }catch (InterruptedException e){ e.printStackTrace(); } } } class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 5000; i++) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i+"-----MyThread"); } } }
-
线程礼让
线程礼让是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,运行状态/就绪状态---》阻塞状态,将CPU资源让给其他线程来使用。
假如线程甲和线程乙在交替执行,某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了CPU资源,执行业务逻辑,但不代表线程甲一直暂停执行。线程甲只是在特定的时间节点礼让,过了时间节点,线程甲再次进入就绪状态,和线程乙争夺CPU资源。
public static native void yield();
使用方法如下
public class test { public static void main(String[] args) throws InterruptedException { YieldThread1 yieldThread1 = new YieldThread1(); YieldThread2 yieldThread2 = new YieldThread2(); yieldThread1.start(); yieldThread2.start(); } } class YieldThread1 extends Thread { @Override public void run() { for (int i = 0; i < 5000; i++) { //当i=2000到输出语句之间就是YieldThread1礼让的时间 此后500次都是这样 if (i>=2000&&i<=2500){ Thread.yield(); } System.out.println(i+"-----YieldThread1"); } } } class YieldThread2 extends Thread { @Override public void run() { for (int i = 0; i < 5000; i++) { System.out.println(i+"-----YieldThread2"); } } }
要注意礼让的时间很短,所以可能才刚下CPU,马上的又上CPU了
-
线程中断
有很多种情况会造成线程停止运行:
-
线程执行完毕自动停止。
-
线程执行过程中遇到错误抛出异常并停止线程。
-
执行过程中根据需求手动停止。
Java中实现线程中断有如下几个常用方法:
-
public void stop()
-
public void interrupt()
-
public boolean isInterrupted()
stop方法在新版本JDK中已经不推荐使用。因为这就是相当于想关电脑却直接拔电源这种简单粗暴的方式。所以重点关注后两种方法。
interrupt是一个实例方法,当一个线程对象调用该方法时,表示中断当前线程对象。每个线程对象都是通过一个标志位来判断当前是否为中断状态。
isInterrupted就是用来获取当前线程对象的标志位:true表示清除了标志位,当前线程已经中断;false表示没有清除标志位,当前对象没有中断。
当一个线程对象处于不同的状态时,中断机制也是不同的。
创建状态:实例化线程对象,不启动。
public class test { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(); System.out.println(thread.getState()); thread.interrupt(); System.out.println(thread.isInterrupted()); /** * 输出结果为 * NEW * false */ }
NEW表示当前线程对象为创建状态,false表示当前线程并未中断,因为当前线程根本没有启动就不存在 中断,不需要清楚标志位。
运行状态:
具体来说,当对一个线程,调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。public class test { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("线程中"+i); } } }); System.out.println("新建立线程时候的状态"+thread.getState()); thread.start(); System.out.println("线程启动后的状态"+thread.getState()); thread.interrupt(); System.out.println("线程中断后的中断标志"+thread.isInterrupted()); System.out.println("线程中断后的状态"+thread.getState()); } /** * 输出为: * 新建立线程时候的状态NEW * 线程启动后的状态RUNNABLE * 线程中断后的中断标志true * 线程中断后的状态RUNNABLE(因为interrupt只是设置中断标志而不影响线程运行) */ }
总结:
Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。
也就是说,一个线程如果有被中断的需求,那么就可以这样做。
① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。) -
线程同步
-
定义
Java中允许多线程并行访问,同一时间段内多个线程同时完成各自的操作。多个线程同时操作同一个共享数据时,可能会导致数据不准确的问题。
public class test { public static void main(String[] args) { //数据共享需要把这个对象放在外面,而不是放在for循环里面,只有这样才能是数据共享。(虽然操作的是静态变量,但这是Java,讲的是对象,放在for循环里则对象不一样) MyRunnable myRunnable= new MyRunnable(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(myRunnable,"线程"+i); thread.start(); } /** * 输出结果为 * 线程0是当前的第1位访客 * 线程3是当前的第4位访客 * 线程1是当前的第3位访客 * 线程2是当前的第3位访客(甚至出现两个第三的) * 线程5是当前的第6位访客 * 线程4是当前的第5位访客 * 线程6是当前的第7位访客 * 线程7是当前的第8位访客 * 线程9是当前的第10位访客 * 线程8是当前的第9位访客 */ } } class MyRunnable implements Runnable { private static int num; @Override public void run() { num++; System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客"); } }
如果不进行线程同步,可以看到此时输出的结果就是错误了。
class MyRunnable implements Runnable { private static int num; //通过对run方法的加锁可以就不会出错了 @Override public synchronized void run() { num++; System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客"); } }
-
使用
可以通过synchronized关键字修饰方法来实现线程同步。每一个Java对象都有一个内置锁,内置所会保护使用synchronized关键字所修饰的方法,对象要使用该方法就得先获得锁,否则就得处于阻塞状态。
synchronized关键字可以修饰实例方法(即不加static关键字修饰的方法),也可以修饰静态方法,但两者在使用上还是有些区别的:
synchronized修饰静态方法
import java.time.temporal.Temporal; //调用加锁下的静态方法 public class test { public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //调用静态方法 test.testFunction1(); //输出结果为start和end成对出现 /** * start------- * end-------- * start------- * end-------- * ...... */ } }); thread.start(); } } public synchronized static void testFunction1(){ System.out.println("start-------"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end--------"); } }
synchronized修饰非静态方法:
import java.time.temporal.Temporal; public class test { public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //实例化方法 test test = new test(); test.testFunction2(); /** * 输出结果为: * 10个start输出完再输出10个end */ } }); thread.start(); } } public synchronized void testFunction2(){ System.out.println("start-------"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end--------"); } }
加锁静态方法下:由于该方法是所有类共享该方法,其实也就复合“数据共享”这一概念,此时加锁的话对于所有的实例对象都是一样的。如下图所示,大家都是共享该静态方法,所以此时当有一个线程获得了该方法的锁后,其他人都得被阻塞。
加锁实例化方法:由于该实例化方法是每个对象调用自己的实例化方法,所以其实加锁是加给自己的,如果不是同一个对象多次调用同一个方法,则是分别各自加锁了,只对自己有限制。如下图所示各个实例化对象所加锁的方法只是自己的实例化方法。
synchronized还可以修饰代码块,会为代码块加上内置锁,从而实现同步。
public class test { public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { test.testFunction1(); //输出结果为start和end成对出现 /** * start------- * end-------- * ...... */ } }); thread.start(); } } public static void testFunction1(){ //在代码块前加synchronized关键字后还需要再其括号后添加对象 比如该类或该类的某一个实例化对象this synchronized (test.class) { System.out.println("start-------"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end--------"); } } }
如果此时括号内添加的是类, 则该代码块的锁是类级别的是跨对象的,锁的范围是针对类,多个线程访问互斥。 (类似于锁定静态方法)
如果此时括号内添加的是对象实例this, 则该代码块的锁是不可跨对象,所以多个线程不同对象实例访问此方法,互不影响,无法产生互斥。 (类似于锁定非静态方法)
-
总结
1、给实例方法(非静态方法)加synchronized关键字并不能实现线程同步。
2、线程同步的本质是锁定多个线程所共享的数据,比如虽然synchronized修饰的是实例方法,但在实例方法中有静态变量(这种情况一般见于run方法中),此时本质上就是在修饰静态变量。
3、每人一份的东西并不需要排队,如果是多人共享的一个东西就需要排队实现同步。
锁定的资源在内存中是一份还是多份?一份大家需要排队,线程同步,多份(一人一份),线程不同步。
无论是锁定方法还是锁定对象,锁定类,只需要分析这个方法、对象、类在内存中有几份即可。对象一般都是多份(如果只创建一个对象那当然是只有一份了),类一定是一份;方法就看是静态方法还是非静态方法,静态方法一定是一份,非静态方法一般是多份。