@[toc]
线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五 种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的同步(线程的安全问题)
ps:
- 在Java多线程02—实现Runnable接口的方式创建线程练习题1.0和2.0都遇到的重复出票问题便是线程的安全问题
- Java设计模式之单例设计模式中的懒汉式实现也提到过线程的安全问题
线程的安全问题:
- 例如之前多个窗口买票的例题提到的重票、错票等问题
- 问题出现的原因:A线程执行的过程中,B、C、D..等其他线程参与进来,也开始执行相同的过程,导致共享数据的错误。
- 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行,及时线程A出现了阻塞,也不能被改变
多个线程执行的不确定性引起执行结果的不稳定多个线程对账本的共享,会造成操作的不完整性,会破坏数据
理想状态下:
极端状态下:
同步代码块解决线程安全问题
在Java中,通过同步机制,来解决线程的安全问题
处理实现Runnable的线程安全问题
关键字:synchronized
synchronized(同步监视器){
//需要被同步的代码
}
说明:操作共享数据的代码,即为需要被同步的代码
共享数据:多个线程共同操作的变量。比如前面例题的车票sum
同步监视器:俗称”锁“。任何一个类的对象都可以来充当这个”锁“。(要求:多个线程必须共用同一把锁)
代码举例:
package com.jsm.Java3;
public class ThreadTest1 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.start();
t2.start();
t3.start();
}
}
class Window4 implements Runnable{
private int num =100;
Object obj=new Object();//任意对象都可以
@Override
public void run() {
//Object obj=new Object();//"锁"不能放这里,因为这样就不是同一个锁,因为运行一次run方法就new了一次锁
while (true){
synchronized(obj) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖出的票号为:" + num);
num--;
} else {
break;
}
}
}
}
}
处理继承Thread类的线程安全问题
首先要清楚继承Thread类创建线程的方法造过多个对象,不像实现Runnable创建线程那样子只造过一个对象,天然就形成了共享数据,所以处理继承Thread类的线程安全问题与刚刚不同的只有一点,把“锁”设置为静态对象即可
代码:
package com.jsm.Java3;
/*
处理继承Thread类的线程安全问题
*/
public class ThreadTest2 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Window3 w2 = new Window3();
Window3 w3 = new Window3();
w1.start();
w2.start();
w3.start();
}
}
class Window3 extends Thread{
private static int num =100;//车票为100张(关键,这个num变量必须设置为100,不然每一个对象都能调用,应该是一个共用性质的数据)
private static Object obj=new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + num);
num--;
} else {
break;
}
}
}
}
}
附加知识
★提醒★:假如不想去重新new一个对象,可以考虑用别的东西来充当“锁”,关于this看java中关键字this的使用、具体情况具体分析,并且慎用,要考虑this是不是唯一的,也可以考虑使用当前类来充当同步监视器,但是无论如何都要保证,这个“锁”是唯一的,并且用synchronized去操作需要同步的代码时,不能包含代码多了,也不能包含代码少了。
- 在处理实现Runnable的线程安全问题的时候,如果把“锁”换成“this”,也是可行的,this表现当前对象,在上面处理实现Runnable的线程安全问题的代码中如果锁用this,表示当前类的对象,而当前类就是Window3,而Window3只建了一个对象w1,所以this就代表了w1,天然的,在实现Runnable接口的方法去创建线程,处理安全问题的时候锁可以直接用this。附加部分代码
synchronized(this) {
//需要被同步的代码
}
- 而在处理继承Thread类的线程安全问题的时候不同,此方法中创建过多个对象,此时要明白一个观点,在Java中类虽然类是对象的模板,但是要明白“万物皆对象”,类其实也是对象,如果不理解可以这么去想,类也有父类,类便是父类的对象,而且创建对象的本质就是参照这个类(对象)去创建的,所以在处理继承Thread类的线程安全问题的时候,锁可以换成当前类,而且这里的类是唯一的,windows3.class只会加载一次。附加部分代码
synchronized(windows3.class) {
////需要被同步的代码
}
处理线程安全问题的局限性
好处:同步的方式,解决了线程的安全问题
局限性:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低