多线程认知系列(二)之线程的生命周期及安全问题
多线程系列:
多线程认知系列(一)之认识线程、简单实现线程
上一篇文章已经初步的认识了线程,那么我们继续研究一下,那使用线程肯定会遇到一些问题啊,我们来看看
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态
新建状态:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值
就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
阻塞状态:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
死亡:线程处理完任务,结束运行
由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。
举个售票员卖100张票的栗子:
public class ThreadTicket {
public static void main(String[] args) {
ThreadDemo t1 = new ThreadDemo();
t1.setName("售票员111");
ThreadDemo t2 = new ThreadDemo();
t2.setName("售票员222");
ThreadDemo t3 = new ThreadDemo();
t3.setName("售票员333");
t1.start();
t2.start();
t3.start();
}
}
class ThreadDemo extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "售卖了:" + ticket);
ticket--;
} else {
break;
}
}
}
}
那么,我们运行main方法,会得到以下的结果:
纳尼???竟然卖出了100票号两次,这什么鬼啊,我加了static了啊
那肯定是有问题的了,怎么解决呢?
必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。
synchronized(同步监视器){
//需要被同步的代码块(即为操作共享数据的代码)
}
1、共享数据:多个线程共同操作的同一个数据(变量)
2、同步监视器:由任何一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁
注:在实现的方式中,考虑同步的话,可以使用this来充当锁。但是在继承的方式中,慎用this
代码示例:
class ThreadDemo extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (ThreadDemo.class) {
if (ticket > 0) {
System.out.println(getName() + "售卖了:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
class RunnableTestDemo implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (RunnableTestDemo.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售卖了:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
将操作共享数据的方法声明为synchronized. 即此方法为同步方法,能够保证当其中一个线程执行此方法时,其他线程在外等待直至此线程执行完此方法。
同步方法的锁:this(不用显式的写) ,就是在方法上加个synchronized,相当于进入这个方法是单线程的。
class RunnableFunctionDemo implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (true) {
checkTicket();
if (ticket<=0){
break;
}
}
}
private static synchronized void checkTicket(){
if(ticket > 0) {
try {
sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
ticket--;
}
}
}
主要是lock.lock() 跟 lock.unlock() 结合使用
public class Window {
public static void main(String[] args) {
WindowDemo demo = new WindowDemo();
Thread t1 = new Thread(demo);
t1.setName("t1++++");
Thread t2 = new Thread(demo);
t2.setName("t2++++");
t1.start();
t2.start();
}
}
class WindowDemo implements Runnable {
private static int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket > 0) {
if (ticket % 2 == 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ",售票员出手了:" + ticket);
} else {
break;
}
ticket--;
} finally {
lock.unlock();
}
}
}
}
注意点:ReentrantLock() 参数默认false,如果为true,卖票窗口会交替卖票
总结:
synchronized 跟 lock的区别
相同点:都能解决线程的安全问题
不同点:synchronized 机制在执行完相应的同步代码以后,会自动的释放同步监视器
lock要手动开启同步,在结束的时候也要手动的关闭 unlock()
在实际的工作中,我使用的更多的是实现Runable接口实现自己的业务逻辑,当需要运行里面的一些数据的时候,使用callable,
使用synchronized的概率更大,比如生成流水号等等
那么,这篇文章就到这了,下一篇讲一下死锁问题,以及如何解决,系列文章还有3-4篇啦~