1.多线程概述
多线程的实现由两种,一种是时间片,一种是锁机制。Java中的多线程使用的锁机制。每一个对象都有会有一个锁,通过锁对象来控制调度多线程,也正因为有锁机制,才会使线程有很多的状态。
多进程与多线程:
每一个程序就是一个进程(有点抽象举个栗子,在电脑上我们同时开QQ、微信、chrome,这个三个分别是程序(进程)),而在程序中的每个功能又是一个线程(再举个栗子,qq可以同时与多个人聊天),所以线程是在进程里面的。
1.1java线程中生命周期
多线程的生命周期:
这张图中详细的描述了线程的生命周期。
2.synchronized关键字
synchronized
关键字,在多线程中同步的关键,其可以修饰方法或者,直接在代码块中使用,注意,不能修饰变量与类使用时必定其中是有一个锁对象的,这样才有同步的意义,而在修饰方法时其锁对象是方法所在类本身(this),使用到代码块中可以任意传入对象,但是要根据场景来做选择,传入什么样的对象才可以实现线程同步。
3.Thread类API
3.1sleep()与wait()
sleep():
- sleep方法使当前线程休眠,是Thread类中方法,需要传入时间
- 调用sleep方法后,当前线程处于休眠状态,并不释放锁,其他线程不可能抢到CPU执行权
- sleep当休眠时间到后自动醒来执行剩下(同步)代码,然后才回释放锁。
- sleep最好不要使用在步代码块中。
wait():
- wait方法是Object类中,也就是说每个类都有此方法,也证明了每个对象都有一把锁
- wait方法可以传入时间,也可以不传。线程等待时,是释放锁的,其他线程的同步代码块可以执行
- wait方法当不传入时间时,需要notify或者notifAll方法主动唤醒
- wait方法调用只能在同步代码块中(也就是
synchronized
关键字修饰的域中)
注意:这里我有一个疑惑的地方,在多线程的生命周期图
中,我们发现经过运行状态到堵塞,堵塞完了之后又回到了,可运行状态(这个状态是不能保证一定被执行的,需要抢到锁了才有执行权),而从使用与描述中。发现sleep好像不是这个过程,而是sleep堵塞完之后又回到的运行状态,这里我的想法是,可能是锁
与线程生命周期
的概念混淆了,当线程sleep之后确实是进入了堵塞状态,但是没有释放其线程所拥有的锁,导致其他线程不能运行,而当其休眠时间到后,这时确实是回到了可运行状态
,但是sleep所在的同步代码块还是没有释放锁的,也就说是一个可运行状态且拥有锁的线程
,这时就直接进入了运行状态,执行完后才释放锁,其他线程才可以抢锁执行。
3.2join()
join的作用:当前线程等待子线程运行完后在向下执行。
应用场景:在主线程中开启了多个子线程一起完成一项任务处理,而可能子线程处理的时间过长,主线程早已结束,但是主线程却需要子线程计算的结果。这时就需要用到join()方法。
测试join()
public class ThreadTest {
//测试类
static class MyThreadOne extends Thread{
//构造函数,传入线程名
public MyThreadOne(String name) {
//调用父类构造函数
super(name);
}
@Override
public void run() {
System.out.println("线程:"+getName()+"开始");
for (int i = 0; i < 4; i++) {
System.out.println(i+" "+getName());//getName()可以获取所在线程的名称
try {
//这里线程睡眠,模拟此线程有很多业务逻辑在执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程:"+getName()+"结束");
}
}
}
测试一:不加join时,线程的执行过程
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"线程开始");
MyThreadOne a = new MyThreadOne("a");
a.start();
System.out.println(Thread.currentThread().getName()+"线程结束");
}
输出的结果:
main线程开始
main线程结束 //可以看出主线程运行的非常快,执行完创建a线程的代码就结束了,这时连JVM还没有初始化线程
线程:a开始
0 a
1 a
2 a
3 a
线程:a结束
测试二:加入join不传参数
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"线程开始");
MyThreadOne a = new MyThreadOne("a");
a.start();
try {
//加入
a.join();
System.out.println("join执行完了~");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程结束");
}
输出结果为:
main线程开始
线程:a开始
0 a
1 a
2 a
3 a
线程:a结束
join执行完了~ //这里发现当join不传入时间时,主线程会等待子线程全部处理完后才会向下执行
main线程结束
测试三:join传入参数(时间)
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"线程开始");
MyThreadOne a = new MyThreadOne("a");
a.start();
try {
a.join(1000);
System.out.println("a join执行完了");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程结束");
}
打印结果为:
main线程开始
线程:a开始
0 a
a join执行完了 //这里看到首先第一点,join是在main之前执行完。
main线程结束 //当join传入时间过了主线程便会向下执行,不再等待a线程。
1 a
2 a
3 a
线程:a结束
测试四:是哪个线程掉用就只等待哪个线程执行完吗,是否等待所有线程?
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"线程开始");
//创建线程a
MyThreadOne a = new MyThreadOne("a");
//创建线程b
MyThreadOne b = new MyThreadOne("b");
//开启线程a
a.start();
try {
//模拟 a线程的开启与b线程的开启之间有很多的业务逻辑(保证其不是很快的同时开启)
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//开启线程b
b.start();
try {
//a线程调用join
a.join();
System.out.println("a join执行完了~");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程结束");
}
打印结果:
main线程开始
线程:a开始
0 a
1 a
线程:b开始
0 b
2 a
3 a
1 b
线程:a结束 //当a线程结束后
a join执行完了~ //主线程打印 a join执行完了
main线程结束 //主线程向下执行到结束
2 b
3 b
线程:b结束 //b线程由于晚一些开启所以,最后结束。
3.2.1join总结:
- join是Thread类中的方法,且不是静态方法,所以必须是开启了的线程才能调用
- join是将调用的线程,会使主线程堵塞,等待被调用线程执行完后再继续主线程执行
- join若传入时间,那么会主线程会等待调用线程执行指定时间后再继续执行。
- 多个子线时,那个线程调用join主线程就等待那个线程执行完。
4.多线程编码
测试类:
public class SyncThreadTest{
static class Printer{
public void print1(){
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.println();
}
public void print2(){
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.println();
}
}
}
大致解释一下这个类,SyncThreadTest
类中有一个Printer
类内类,需要开两个线程来分别执行Printer printer =new Printer
的print1
与print2
,想要的效果是这两个方法互斥。例如:当一个线程执行print1()
时,另一个线程需要执行的print2()
堵塞,后面所以的实验都用到的是这个类。
4.1同步代码块
4.1.1Junit测试多线程问题
@Test
public void testSynThread(){
System.out.println("test开始");
Printer printer = new Printer();
new Thread(){
@Override
public void run() {
//无限循环是为了在控制台中打分辨是否方法互斥,因为CPU执行实在太快了。
while (true){
printer.print1();
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true){
printer.print2();
}
}
}.start();
System.out.println("test结束");
}
//输出结果竟然是;
//第一次:
//test开始
//123
//test结束
//abc
//第二次:
//test开始
//test结束
怎么会输出这样的结果?网上查了一下,原来Junit并不支持多线程测试。测试不管子线程是否结束都会回调TestResult的wasSuccessful方法,然后判断结果是成功还是失败,最后调用相应的System.exit()方法~~,太坑了。。还是老老实实用main方法来测试吧。
4.1.2main方法测试
public static void main(String[] args) {
System.out.println("test开始");
Printer printer = new Printer();
new Thread(){
@Override
public void run() {
//无限循环是为了在控制台中打分辨是否方法互斥,因为CPU执行实在太快了。
while (true){
printer.print1();
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true){
printer.print2();
}
}
}.start();
System.out.println("test结束");
}
原始类不加添加任何同步时:
123
123
123
123
12abc
abc
abc
123
123
1abc
abc
abc
控制台打印的太多,这里只截取出错误的地方。明显这是不符合我们的预期的(想要两线程调用同一对象的两个方法是互斥的)
添加synchronized:
static class Printer{
String s = new String();
public void print1(){
synchronized (s){
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.println();
}
}
public void print2(){
synchronized (s){
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.println();
}
}
}
输出结果:
abc
abc
abc
abc
123
123
123
//是没有出现一行中有字母与数字的,说明同步成功。读者可以自己去实验一下,看是否正确。
注意:同步代码块是必须要同一对象的锁的,如果是不同对象,那么将不会其效果。
4.2同步方法
//当一个方法上添加synchronized关键字,如何保证另一个方法的同步?
public synchronized void print1(){
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.println();
}
public void print2(){
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.println();
}
这时想要这两个方法同步的方法有两种:
- 同步代码块中锁对象为
this
public void print2(){ synchronized (this){ System.out.print("a"); System.out.print("b"); System.out.print("c"); System.out.println(); } }
- 同样需要同步的方法上添加
synchronized
关键字public synchronized void print2(){ System.out.print("a"); System.out.print("b"); System.out.print("c"); System.out.println(); }
总结思考:
我的理解是,当在方法上添加synchronized
关键字之后,因为没有像 代码块那样声明 同步对象,所以java将其同步对象默认为此对象,即是this
,所以在如果其他代码块需要于此互斥时需要传入this
这个锁对象。或者其方法也用synchronized
修饰,重点静态方法被synchronized
修饰又是不同锁对象,所以需要注意。接下来将会讲到。
4.3同步静态方法
//当静态方法被synchronized修饰时,如何让其他代码与此互斥?
public static synchronized void print1() {
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.println();
}
上小节中说 静态方法 与 非静态方法的锁是不同的,这里就来做一个实验:
//静态方法 被synchronized
public static synchronized void print1() {
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.println();
}
//非静态方法 被synchronized
public synchronized void print2() {
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.println();
}
输出的结果:
123
123
123
123abc
abc
abc
abc
//出现同行,字母与数字 说明 他们两确实不是同一锁。
如果想要同步静态方法的代码块需要如何做呢?
- 方法同样为静态且被synchronized修饰
public static synchronized void print2() { System.out.print("a"); System.out.print("b"); System.out.print("c"); System.out.println(); }
- 代码块中synchronized的锁对象传入其方法所在类的
class对象
public void print2() { synchronized (Printer.class){ System.out.print("a"); System.out.print("b"); System.out.print("c"); System.out.println(); } }
4.4案例一(多窗口售票)
了解了上面的这些知识之后可以用一个案例来加深知识理解。
实现Runnable接口
/**
* Created by CDz_ on 2017/11/27.
* 多线程售票
* 实现Runnable接口写法
*/
public class SyncThread_Demo1 {
public static void main(String[] args) {
TicketThread ticketThread = new TicketThread();
Thread a = new Thread(ticketThread,"A");
Thread b = new Thread(ticketThread,"B");
Thread c = new Thread(ticketThread,"C");
Thread d = new Thread(ticketThread,"D");
a.start();
b.start();
c.start();
d.start();
}
static class TicketThread implements Runnable {
//定义票的总数
private int ticket = 100;
@Override
public void run() {
//获取当前线程的名字
String name = Thread.currentThread().getName();
while (true) {
//同步代码块
synchronized (this) { //this 与 TicketThread.class 都可以
if (ticket <= 0) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "线程窗口售出,一共还剩:" + ticket-- + "张票");
}
}
}
}
}
以上代码有两点需要注意:
- 同步代码块是在while里面而非外面,因为这里如果在外面那么就会使一个线程执行完全部的售票操作。
- 锁对象的使用,在main方法中我们只
new
出了一个TicketThread
对象,故在JVM
堆中只有一个次对象。所以使用this是可以,当然TicketThread.class
也是可以的,因为JVM
中只有一个TicketThread.class
对象。
继承Thread类
/**
* 多线程售票
* 继承Thread类
*/
public class SyncThread_Demo2 {
public static void main(String[] args) {
new TicketThread("A").start();
new TicketThread("B").start();
new TicketThread("C").start();
new TicketThread("D").start();
}
static class TicketThread extends Thread{
//使用静态变量,因为在main方法中new出了多个此类的实例。
//如果是成员变量,不会对象中共享,只有静态变量多实例中才回共享。
private static int ticket = 100;
public TicketThread(String name) {
super(name);
}
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true) {
//这里锁对象必须是要此类的.class对象,因为只有他才是唯一的,当然也可以new出一个静态应用变量作为锁对象。
//这里不能用this的原因同 private static int ticket = 100; 使用静态变量道理一样。(这是JAVA基础方面的,看不懂的同学可以去复习一下JAVA基础知识)
synchronized (TicketThread.class) {
if (ticket <= 0) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "线程窗口售出,一共还剩:" + ticket-- + "张票");
}
}
}
}
}
4.5案例二(多线程通信)
为什么要多线程通信?
上列的一些列子可以看出,CPU执行每个任务是分时间片的,而且每个线程抢夺资源也是随机的(注意千万不要过度依赖线程的优先级),那么就会出现这样一个场景,多线程处理,但是又需要线程之间按我们想要的顺序来执行,就需要多线程之间通信,一个线程执行完了别的线程才可以去执行,为了应对这样的场景故需要多线程之间通信其通信的基础是使用到
wait
notify
notifyAll
,这三个Object
类中函数。
先简单一点:两个线程通信:
/**
* @author CDz_
* @create 2017-11-28 14:44
* 案例场景,因为CPU执行是无序的,通过wait notify notifyall 控制多线程,使其有序
* 实现输出顺序为:
* 123456
* abcdef
**/
public class NotifyThread_Demo1 {
public static void main(String[] args) {
Printor printor = new Printor();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
printor.print1();
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
printor.print2();
}
}
}.start();
}
}
class Printor {
//通信标识
private int flag = 1;
public synchronized void print1() {
//通过判断是否为1,让线程堵塞
if (flag != 1) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(1);
System.out.print(2);
System.out.print(3);
System.out.print(4);
System.out.print(5);
System.out.print(6);
System.out.println();
//执行完需要输出的代码之后,将标识设置为第二个线程通信的条件,也确保此线程睡眠。
this.flag = 2;
//唤醒在等待的线程(也就是第二个线程)
this.notify();
}
public synchronized void print2() {
//判断是否为2,让线程堵塞
if (flag != 2) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.print("d");
System.out.print("e");
System.out.print("f");
System.out.println();
//同理执行完之后,第一个线程通信的条件,也确保此线程睡眠
flag = 1;
//唤醒在等待的线程(也就是第一个线程)
this.notify();
}
}
注意:
此时我使用的是用synchronized
修饰方法,也可以使用synchronized
代码块,使用synchronized
代码块有两点注意
- 需要将整个方法里的内容都要包括进代码块中。(使用
wait
notify
,必须要在同步代码块中,不然运行时会报错java.lang.IllegalMonitorStateException
)。 - 使用锁中传入的对象可以是
this
,也可是Printor.class
。
三个或三个以上线程通信:
/**
* @author CDz_
* @create 2017-11-28 21:58
* 三个线程或三个线程以上通信
* 实现输出顺序为:
* 123456
* abcdef
* ABCDEF
**/
public class NotifyAll_Demo1 {
public static void main(String[] args) {
Printor printor = new Printor();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
printor.print1();
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
printor.print2();
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
printor.print3();
}
}
}.start();
}
static class Printor {
private int flag = 1;
public synchronized void print1() {
//注意这里为什么要使用 while循环 而不是if做判断
//因为notifyAll,是唤醒所有的堵塞现象,如果用if那么线程被唤醒后就会直接往下执行代码,
//而这时可能这个线程是不符合条件的。所以需要用while来做为判断,
//建议多线程通信时,wait方法都用while来做判断。
while (flag != 1) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(1);
System.out.print(2);
System.out.print(3);
System.out.print(4);
System.out.print(5);
System.out.print(6);
System.out.println();
//使第二线程判断放行
this.flag = 2;
//这里使用notifyAll,而不使用notify的原因:
//因为notify只能随机的唤醒一个线程,他可能唤醒的是不符合条件的线程,
//这时就不符合条件的线程经过while循环判断,又进入堵塞导致所有线程都在堵塞JVM就会报错
this.notifyAll();
}
public synchronized void print2() {
while (flag != 2) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.print("d");
System.out.print("e");
System.out.print("f");
System.out.println();
//使第三线程判断放行
flag = 3;
this.notifyAll();
}
public synchronized void print3() {
while (flag != 3) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("A");
System.out.print("B");
System.out.print("C");
System.out.print("D");
System.out.print("E");
System.out.print("F");
System.out.println();
//使第一线程判断放行
flag = 1;
this.notifyAll();
}
}
}
4.6小结:
- 当需要同步普通多代码块时,可以是任意的对象锁,但是必须保证同一对象
- 当同步非静态方法时,其锁是
this
,此对象本身 - 当同步静态方法时,其锁是方法所在类的
class对象
- 实现Runnable接口与继承Thread类在做代码互斥时,锁对象是的使用是有区别的
- 实现Runnable接口,在使用多线程时只需要new一次,所以成员变量不需要是静态,锁对象也可以是this或者此类的class对象
- 继承Thread类,使用多线程时需要new多次,所以成员变量必须是静态的(这里才可以多实例共享),而锁对象必须是此类的class对象