时间真的是快,经不起浪费啊!加油!
当多个线程访问同一个资源时,由于每个线程访问同一份资源的时候,会有时间差。所以很有可能多个线程同时进入同一份资源,然后使得资源的自身信息没有及时得到更新,造成错误输出的情况出现,这就是所谓的线程不安全。为了确保资源的安全,也就是确保线程安全,我们使用关键字synchronized,对需要确保安全的代码进行同步处理。
使用synchronized的基本原理是:当已经有线程进入资源时,此时计算机会给当前资源一把锁,锁住当前资源,其他的线程只能在外部进行等待,线程被阻塞挂起。当访问该资源的线程结束访问的时候,系统会将该锁释放,整个程序进入运行状态,这样就避免了多个进程同时访问同一份资源的问题。
有两种方法可确保线程的同步:
方法1、同步方法:
synchronized
方法2、同步块:
synchronized(引用类型 | this | 类.class){
}
代码语言:javascript复制public class SynDemo01 {
public static void main(String[] args) {
//新建实体对象
Web12306 web = new Web12306();
//创建代理
Thread t1 = new Thread(web,"黄牛1");
Thread t2 = new Thread(web,"黄牛2");
Thread t3 = new Thread(web,"黄牛3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
class Web12306 implements Runnable{
private int num = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
test3();
}
}
//线程不安全
private void test1() {
if (0 >= num) {
this.flag = false;//跳出循环
return;
}
try {
Thread.sleep(100);//模拟网络延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() "抢到了第" num-- "张票");
}
//线程安全
private synchronized void test2() {
if (0 >= num) {
this.flag = false;//跳出循环
return;
}
try {
Thread.sleep(500);//模拟网络延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() "抢到了第" num-- "张票");
}
//线程安全 资源锁定正确
private void test3() {
synchronized(this) {
if (0 >= num) {
this.flag = false;//跳出循环
return;
}
try {
Thread.sleep(500);//模拟网络延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() "抢到了第" num-- "张票");
}
}
}
解析:test1方法中,没有加入synchronized关键字进行同步。使用此方法的时候,会导致线程"黄牛1","黄牛2","黄牛3"同时进入引用类对象"Web12306"当中访问剩余的票数,所以输出的结果会有:“黄牛1抢到了第-1张票”,这种明显错误的结果,就是因为未同步而造成的。
test2方法中,使用的是利用synchronized关键字锁定整个方法,也就是我们上面介绍的方法1:同步方法。将整个方法进行同步处理。但是正如我们所讲述的原理一样,同步方法的关键就在于阻塞线程,所以阻塞的内容越多,整体的运行速度会明显下降。最终造成低效率的结果。
test3方法中,使用的是我们介绍的方法2:同步块。在此方法中我们可以根据自己的分析,判断哪一个地方最有可能出现安全隐患,然后加入同步块,这样就可以适当的减少相应的阻塞内容,在一定的程度上提高代码运行效率。
二、死锁
在我们使用多个同步的时候,假如我们的多线程访问的资源相互同步,然后每个线程都不释放自己的锁,那么就很容易造成死锁的情况。此时,所有的线程都会被挂起,然后相互等待,一直到系统奔溃。所以过多的同步容易造成死锁。
解决死锁的一种方式:生产者与消费者模式
当生产者进行生产操作的时候,消费者被挂起,停止消费;当消费者在消费的时候,生产者被挂起,消费者进行消费。可以使用一种信号灯法进行操作。
信号灯法:
1、wait():等待,释放锁
2、notify()/notifyAll():唤醒
与synchronized一起使用
第一步:我们创建一个电影院场景,其中包含有play(生产者)和watch(消费者)
代码语言:javascript复制public class Movie {
private String pic;
//信号灯
//flag---->T 生产者生产,消费者等待,生产完成后通知消费
//flag---->F 消费者消费,生产者等待,消费完成后通知生产
private boolean flag = true;
/**
* 播放,相当于生产者
* @param pic
*/
public synchronized void play(String pic) {
if(!flag) {//即:生产者等待
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//生产者生产
try {
Thread.sleep(500);//模拟生产了500毫秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//生产结束
this.pic = pic;
System.out.println("生产了:" pic);
//生产者停下
this.flag = false;
//通知消费者
this.notifyAll();
}
/**
* 观看,相当于消费者
*/
public synchronized void watch() {
if(flag) {//生产者在生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//消费者消费
try {
Thread.sleep(200);//假设消费200毫秒就停下
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//消费结束
System.out.println("消费了:" pic);
//消费者停下
this.flag = true;
//通知生产者生产
this.notifyAll();
}
}
第二步:我们创建相应的生产者player和消费者watcher,对同一份资源movie进行访问。
代码语言:javascript复制public class Player implements Runnable {
private Movie m ;
public Player(Movie m) {
super();
this.m = m;
}
@Override
public void run() {
for(int i = 0; i < 20 ;i ) {
if(0 == i%2) {
m.play("左青龙" i);
}else {
m.play("右白虎" i);
}
}
}
}
代码语言:javascript复制public class Watcher implements Runnable {
private Movie m ;
public Watcher(Movie m) {
super();
this.m = m;
}
@Override
public void run() {
for(int i = 0; i < 20 ;i ) {
m.watch();
}
}
}
第三步:对生产者和消费者进行应用
代码语言:javascript复制public class App {
public static void main(String[] args) {
Movie m = new Movie();
//共享资源
Player p = new Player(m);
Watcher w = new Watcher(m);
new Thread(p).start();
new Thread(w).start();
}
}
第四步:查看一下运行结果
代码语言:javascript复制生产了:左青龙0
消费了:左青龙0
生产了:右白虎1
消费了:右白虎1
生产了:左青龙2
消费了:左青龙2
生产了:右白虎3
消费了:右白虎3
生产了:左青龙4
消费了:左青龙4
生产了:右白虎5
消费了:右白虎5
生产了:左青龙6
消费了:左青龙6
生产了:右白虎7
消费了:右白虎7
生产了:左青龙8
消费了:左青龙8
生产了:右白虎9
消费了:右白虎9
生产了:左青龙10
消费了:左青龙10
生产了:右白虎11
消费了:右白虎11
生产了:左青龙12
消费了:左青龙12
生产了:右白虎13
消费了:右白虎13
生产了:左青龙14
消费了:左青龙14
生产了:右白虎15
消费了:右白虎15
生产了:左青龙16
消费了:左青龙16
生产了:右白虎17
消费了:右白虎17
生产了:左青龙18
消费了:左青龙18
生产了:右白虎19
消费了:右白虎19
解析:对最终的结果,可以明显看出所有线程都是一种规律性的出现,不会是随机出现的结果。在线程等待的时候需要注意一点:wait是将线程进行阻塞挂起,并且释放锁。而sleep方法,仅仅是将线程挂起,不释放锁。所以当我们使用sleep的时候,将会使得整个线程阻塞相应的时间后,再重新开始运行。与此同时,其他线程的状态并不会有所改变。
三、任务调度
了解一个类:Timer()
主要用于任务在不同时间的执行情况,具体使用如下所示:
代码语言:javascript复制public class TimeDemo01 {
public static void main(String[] args) {
Timer time = new Timer();
//语法:schedule(TimerTask task, Date firstTime, long period)
time.schedule(new TimerTask() {//使用匿名内部类
@Override
public void run() {
System.out.println("所谓的线程,也就是换一种类,然后运行run里面的代码");
}},new Date(System.currentTimeMillis() 2000),//当前时间过后两秒开始运行
2000);//每间隔2秒运行1次
}
}
Timer类主要是使用schedule方法,该方法主要的几条语句如下所示:
仅将线程运行一次:
schedule(TimerTask task, Date time)
schedule(TimerTask task, long delay)
间隔period时间后,再运行的语句:
schedule(TimerTask task, long delay, long period)
schedule(TimerTask task, Date firstTime, long period)
注意:在使用schedule的时候,我们涉及到了TimerTask类别,这个类别实现了Runnable接口,所以在创建该类别的时候,就可以将其当做一个实现了Runable接口的类来处理,直接新建之后,重写它的Run()方法就好了。
结合上一篇文章,我们对线程进行总结,同时结束线程的学习,进入下一个内容:
一、创建线程 重点
1、继承 Thread
2、实现 Runnable
3、实现 Callable (了解即可)
二、线程的状态
1、新生--->start--->就绪---->运行--->阻塞--->终止
2、终止线程(重点)
3、阻塞:join yield sleep(不是释放锁)
三、线程的信息
1、Therad.currentThread
2、获取名称 设置名称 设置优先级 判断状态
四、同步:多线程使用同一份资源
synchronized (引用类型变量|this|类.class){
}
修饰符 synchronized 方法的签名{
方法体
}
过多的同步可能造成死锁。
五、生产者消费者模式
六、任务调度