三、线程控制
1.join线程
Thread提供了让一个线程等待另一个线程完成的方法—join()方法。当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()线程执行完为止。
join()方法通常在使用多线程的程序中被调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有小问题都得到处理后,再调用主线程进一步操作。
join()方法有如下三种使用形式:
- join():等待被join的线程执行完成。
- join(long millis):等待被join的线程的时间最长为millis毫秒,如果在这个时间内被join的线程还没有执行结束,则不再等待。
- join(long millis, int nanos):等待被join的线程的时间最长为millis毫秒加nanos毫微秒。
第三种方法很少使用,因为程序对时间的精度无须精确到毫微秒,计算机本身也无法精确到毫微秒。
class MyThread extends Thread{@Overridepublic void run() {System.out.println("子线程");}}public class Test {public static void main(String[] args) throws InterruptedException {MyThread myThread = new MyThread();myThread.start();myThread.join();//在main线程中加入子线程,则只有当该子线程执行完后main线程才会执行System.out.println("main线程");}}
2.后台线程
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程”(Daemon Thread),又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程的特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可以将指定的线程设置为后台线程,该方法必须在start()方法之前设置。主线程默认是前台线程,并不是所有的线程默认都是前台线程,前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。可以使用Thread类的isDaemon()方法判断指定的线程是否为后台线程。
class MyThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("后台线程: " + i);}}}public class Test {public static void main(String[] args) throws InterruptedException {MyThread myThread = new MyThread();myThread.setDaemon(true);//将此线程设置为后台线程,必须在start之前设置myThread.start();//启动后台线程System.out.println(myThread.isDaemon());//判断该线程是否为后台线程for (int i = 0; i < 10; i++) {System.out.println("main线程 " + i);}//————————程序执行到这里,前台线程(main线程)结束————————//后台线程也随着结束}}
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令到做出响应需要一定的时间,不是前台线程一结束,后台线程马上死亡,中间会有一定的时间,在这段时间内,后台线程仍然继续执行。
3.线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类静态方法sleep()方法来实现,sleep()方法有两种形式:
- static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度影响。
- static void sleep(long millis, int nanos):暂停时间为millis毫秒加nanos毫微秒。
第二种形式很少使用。
当线程调用sleep()方法进入阻塞状态后,在睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会被执行,因此sleep()常用来暂停程序的执行。
public class Test {public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {System.out.println("main线程 " + i);Thread.sleep(1000);//让当前线程暂停1秒}}}
4.yield()方法
Thread类还提供了一个yield()静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让这个线程进入就绪状态,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用yield()方法暂停一下后,又被重新调度执行。实际上,当某个线程调用yield()方法后,只有优先级与当前线程相同或者比当前线程优先级更高的处于就绪状态的线程才会获得执行的机会。
sleep()和yield()的区别:
- sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同或者优先级更高的线程执行机会。
- sleep()方法会将线程转入阻塞状态,只有经过阻塞时间后才会重新进入就绪状态;而yield()方法只是将线程转入就绪状态。
- sleep()方法声明抛出了InterruptedException异常,所有调用sleep()方法时要捕获该异常,或者声明抛出该异常;而yield()方法没有抛出任何异常。
sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
4.设置线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会,不是说高优先级的进程一定在低优先级的进程之前执行。每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数是一个整数,范围在1~10之间,也可以使用Thread类的如下三个静态常量:MAX_PRIORITY:10
- MIN_PRIORITY:1
NORM_PRIORITY:5(默认优先级)
class MyThread extends Thread{public MyThread(String name){super(name);}@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + ",优先级是: " + getPriority() + ",循环变量i为: " + i);}}}public class Test {public static void main(String[] args) throws InterruptedException {System.out.println("主线程默认优先级为: " + Thread.currentThread().getPriority());Thread.currentThread().setPriority(6);//改变主线程优先级for (int i = 0; i < 30; i++) {if (i == 10) {var low = new MyThread("低级");low.start();System.out.println("low线程创建之初的优先级 " + low.getPriority());low.setPriority(Thread.MIN_PRIORITY);//设置线程为最低优先级}if (i == 20) {var high = new MyThread("高级");high.start();System.out.println("high线程创建之初的优先级 " + high.getPriority());high.setPriority(Thread.MAX_PRIORITY);//设置线程为最高优先级}}}}结果:主线程默认优先级为: 5low线程创建之初的优先级 6high线程创建之初的优先级 6高级,优先级是: 6,循环变量i为: 0高级,优先级是: 10,循环变量i为: 1高级,优先级是: 10,循环变量i为: 2高级,优先级是: 10,循环变量i为: 3高级,优先级是: 10,循环变量i为: 4高级,优先级是: 10,循环变量i为: 5高级,优先级是: 10,循环变量i为: 6高级,优先级是: 10,循环变量i为: 7高级,优先级是: 10,循环变量i为: 8高级,优先级是: 10,循环变量i为: 9低级,优先级是: 1,循环变量i为: 0低级,优先级是: 1,循环变量i为: 1低级,优先级是: 1,循环变量i为: 2低级,优先级是: 1,循环变量i为: 3低级,优先级是: 1,循环变量i为: 4低级,优先级是: 1,循环变量i为: 5低级,优先级是: 1,循环变量i为: 6低级,优先级是: 1,循环变量i为: 7低级,优先级是: 1,循环变量i为: 8低级,优先级是: 1,循环变量i为: 9
注:虽然Java提供了10个优先级级别,但这些优先级级别需要操作系统的支持,不同操作系统上的优先级并不相同,而且不能很好地和Java的10个优先级对应,因此应该尽量避免直接为线程指定优先级,而应该使用者三个静态常量来设置优先级,这样才可以保证程序具有最好的可移植性。
四、线程同步
1.线程安全问题
当使用多个线程共享同一个数据时,由于操作的不完整性,很容易出现线程安全问题,破坏数据。
继承Thread类方式卖票程序
class Window extends Thread{private static int ticket = 10;//静态变量,实现共享@Overridepublic void run() {while (true){if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;} else {break;}}}}public class Test {public static void main(String[] args){Window t1 = new Window();Window t2 = new Window();Window t3 = new Window();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}窗口3卖票,票号为:10窗口1卖票,票号为:10窗口2卖票,票号为:10窗口3卖票,票号为:7窗口1卖票,票号为:6窗口2卖票,票号为:5窗口3卖票,票号为:4窗口1卖票,票号为:4窗口2卖票,票号为:3窗口3卖票,票号为:1窗口2卖票,票号为:1窗口1卖票,票号为:1
实现Runnable方式卖票
class Window implements Runnable{private int ticket = 10;//Runnable方式原生共享变量@Overridepublic void run() {while (true){if(ticket > 0){try {Thread.sleep(100);//在这里增加线程切换概率} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket --;}else{break;}}}}public class Test {public static void main(String[] args){Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}窗口3卖票,票号为:10窗口2卖票,票号为:10窗口1卖票,票号为:10窗口3卖票,票号为:7窗口2卖票,票号为:6窗口1卖票,票号为:5窗口2卖票,票号为:4窗口3卖票,票号为:4窗口1卖票,票号为:2窗口2卖票,票号为:1窗口3卖票,票号为:1窗口1卖票,票号为:-1
2.解决方式一:同步代码块
为了解决线程安全问题,Java的多线程支持引入了同步监视器,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized(同步监视器){//需要被同步的代码,即操作共享数据的代码}
synchronize方法中的参数就是同步监视器,俗称锁。线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
Java允许任何对象充当同步监视器,但同步监视器的目的是阻止多个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器,逻辑为:加锁-修改-释放锁。任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。通过这种方式,可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。
同步监视器:
(1)任何一个对象都可以充当同步监视器。
(2)多个线程必须共用这个同步监视器。2.1 Runnable实现方式
class Window implements Runnable{private int ticket = 10;private Object obj = new Object();//同步监视器,Runnable方式原生共享变量@Overridepublic void run() {while (true){//synchronized (ticket) {这里ticket为基本数据类型,不是对象,无法作为同步监视器synchronized (obj) {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;} else {break;}}}}}public class Test {public static void main(String[] args){Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}
这里还需专门定义一个同步监视器,有没有更简便可以直接使用的同步监视器?this可以作为同步监视器,且在实现Runnable方式中是唯一的,因为只创建了一个Window对象。
class Window implements Runnable{private int ticket = 10;@Overridepublic void run() {while (true){//this指向调用该方法的对象,是唯一的,即下面所创建的Window对象synchronized (this) {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;} else {break;}}}}}public class Test {public static void main(String[] args){Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}
2.1 继承Thread类
class Window extends Thread{private static int ticket = 10;//静态变量,实现共享private Object obj = new Object();//非静态变量,在继承Thread类方式中不会被多个线程共享@Overridepublic void run() {while (true){//synchronized (this) {synchronized (obj) {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;} else {break;}}}}}public class Test {public static void main(String[] args){Window t1 = new Window();Window t2 = new Window();Window t3 = new Window();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}窗口2卖票,票号为:10窗口1卖票,票号为:10窗口3卖票,票号为:10窗口2卖票,票号为:7窗口1卖票,票号为:7窗口3卖票,票号为:5窗口3卖票,票号为:4窗口2卖票,票号为:3窗口1卖票,票号为:2窗口2卖票,票号为:1窗口3卖票,票号为:0窗口1卖票,票号为:-1
注:有问题,这样还是有安全问题,为什么?同步监视器在这里不唯一,有三个,三个线程不是共用这一个同步监视器。在这里,也不能使用this来作为同步监视器,因为Window对象不唯一,this所指向的对象也不唯一。
class Window extends Thread{private static int ticket = 10;//静态变量,实现共享private static Object obj = new Object();//设置为静态变量,让多个线程共享这个同步监视器@Overridepublic void run() {while (true){//方式一synchronized (obj) {//方式二:使用Window类作为对象充当同步监视器,Window.class只会加载一次,是唯一的synchronized (Window.class) {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;} else {break;}}}}}public class Test {public static void main(String[] args){Window t1 = new Window();Window t2 = new Window();Window t3 = new Window();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}
2.3 总结
在实现Runnable方式中,可以考虑使用this作为同步监视器,是考虑,不是一定可以,要看this所指向的对象是不是唯一。
- 在继承Thread方式中,要慎用this作为同步监视器,可以考虑使用当前类充当同步监视器。
- 同步代码块部分不能包含太多,也不能包含太少,一定要把对共享数据操作的代码包含在内。包含太多,效率变低是一方面,更重要的一方面可能导致程序错误。
总之,一定要保证同步监视器唯一。
class Window extends Thread{private static int ticket = 10;//静态变量,实现共享private static Object obj = new Object();//设置为静态变量,让多个线程共享这个同步监视器@Overridepublic void run() {synchronized (Window.class) {//同步代码块包含了while(true)部分,此时有问题,只有一个线程卖票,与题意不符while (true){if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;} else {break;}}}}}public class Test {public static void main(String[] args){Window t1 = new Window();Window t2 = new Window();Window t3 = new Window();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}窗口1卖票,票号为:10窗口1卖票,票号为:9窗口1卖票,票号为:8窗口1卖票,票号为:7窗口1卖票,票号为:6窗口1卖票,票号为:5窗口1卖票,票号为:4窗口1卖票,票号为:3窗口1卖票,票号为:2窗口1卖票,票号为:1
3.解决方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们可以将此方法声明为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无需显式指定同步监视器,同步方法的同步监视器默认为this,也就是调用该方法的对象。
3.1 Runnable实现方式
class Window implements Runnable{private int ticket = 10;@Override//这里直接将run()方法声明为同步方法是错误的,包含了while(true)循环public synchronized void run() {while (true){if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;} else {break;}}}}public class Test {public static void main(String[] args){Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}
3.2 继承Thread类
class Window extends Thread{private static int ticket = 10;//静态共享变量@Overridepublic void run() {while (true){show();}}// public synchronized void show(){//此种方式是错误的,同步监视器为t1,t2,t3,不唯一public static synchronized void show(){//将同步方法声明为静态的,此时同步监视器不是this了,是当前类Window.classif (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;}}}public class Test {public static void main(String[] args){Window t1 = new Window();Window t2 = new Window();Window t3 = new Window();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}
3.3 总结
使用同步方法仍然涉及同步监视器,只是不需要我们显式的声明。
非静态的同步方法的同步监视器为this,静态的同步方法的同步监视器为当前类本身。
4.解决方式三:同步锁(Lock)
4.1 Lock介绍
从Java5开始,提供了一种功能更强大的线程同步机制——显式定义同步锁对象来实现同步。
ReentrantLock(可重入锁)是Lock接口的实现类,使用该ReentrantLock对象可以显式地加锁、释放锁。4.2 使用步骤
创建一个ReentrantLock对象。
- 将同步代码放到try语句块里。
- try语句块的前面使用ReentrantLock对象调用lock()方法。
finally语句块里使用ReentrantLock对象调用unlock()方法。
class Window implements Runnable {private int ticket = 10;ReentrantLock lock = new ReentrantLock();//定义锁对象@Overridepublic void run() {while (true) {lock.lock();//加锁try {if(ticket > 0){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);ticket--;}else{break;}} finally {lock.unlock();//释放锁}}}}public class Test {public static void main(String[] args){Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}}
4.3 经典面试问题
synchronized方式和Lock方式的异同?
答:
相同点:都可以用来解决线程安全问题。
不同点:
(1)synchronize的是隐式锁,Lock是显式锁。synchronized机制在执行完同步代码块后,自动的释放同步监视器。Lock需要手动的去启动同步和结束同步。
(2)Lock只有代码块锁,而synchronized有代码块锁和方法锁。
(3)使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。
- 如何解决线程的安全问题?有几种方式?
答:使用Java提供的同步机制来解决线程安全问题,一共有三种方式:
(1)同步代码块。
(2)同步方法。
(3)同步锁。
