小结:
到目前位置,介绍了线程离开运行状态的3种方法:
1、调用Thread.sleep():使当前线程睡眠至少多少毫秒(尽管它可能在指定的时间之前被中断)。
2、调用Thread.yield():不能保障太多事情,尽管通常它会让当前运行线程回到可运行性状态,使得有相同优先级的线程有机会执行。
3、调用join()方法:保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。
除了以上三种方式外,还有下面几种特殊情况可能使线程离开运行状态:
1、线程的run()方法完成。
2、在对象上调用wait()方法(不是在线程上调用)。
3、线程不能在对象上获得锁定,它正试图运行该对象的方法代码。
4、线程调度程序可以决定将当前运行状态移动到可运行状态,以便让另一个线程获得运行机会,而不需要任何理由。
线程的同步
如果我们创建的多个线程,存在着共享数据,那么就有可能出现线程的安全问题:当其中一个线程操作共享数据时,还未操作完成,另外的线程就参与进来,导致对共享数据的操作出现问题。
方式一:同步代码块:
synchronized(同步监视器){
//操作共享数据的代码
}
注:1.同步监视器:俗称锁,任何一个类的对象都可以才充当锁。要想保证线程的安全,必须要求所有的线程共用同一把锁!
2.使用实现Runnable接口的方式创建多线程的话,同步代码块中的锁,可以考虑是this。如果使用继承Thread类的方式,慎用this!
3.共享数据:多个线程需要共同操作的变量。 明确哪部分是操作共享数据的代码。
//模拟火车站售票窗口,开启三个窗口售票,总票数为100张 class Window2 implements Runnable { int ticket = 100;// 共享数据 public void run() { while (true) { synchronized (this) {//this表示当前对象,本题中即为w,是否锁的标识 if (ticket > 0) { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } } } } } public class TestWindow2 { public static void main(String[] args) { Window2 w = new Window2(); 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(); } }
方式二:同步方法:将操作共享数据的方法声明为synchronized。
比如:public synchronized void show(){ //操作共享数据的代码}
注:1.对于非静态的方法而言,使用同步的话,默认锁为:this。如果使用在继承的方式实现多线程的话,慎用!
2.对于静态的方法,如果使用同步,默认的锁为:当前类本身。以单例的懒汉式为例。 Class clazz = Singleton.class
class Window4 implements Runnable { int ticket = 100;// 共享数据 public void run() { while (true) { show(); } } public synchronized void show() { if (ticket > 0) { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } } } public class TestWindow4 { public static void main(String[] args) { Window4 w = new Window4(); 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(); } }
即使是线程安全类,也应该特别小心,因为操作的线程是间仍然不一定安全。
举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。
看个代码:
publicclass NameList { private List nameList = Collections.synchronizedList(new LinkedList()); publicvoid add(String name) { nameList.add(name); } public String removeFirst() { if (nameList.size() > 0) { return (String) nameList.remove(0); } else { returnnull; } } } publicclass Test { publicstaticvoid main(String[] args) { final NameList nl =new NameList(); nl.add("aaa"); class NameDropperextends Thread{ publicvoid run(){ String name = nl.removeFirst(); System.out.println(name); } } Thread t1 = new NameDropper(); Thread t2 = new NameDropper(); t1.start(); t2.start(); } }
虽然集合对象
private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,但是程序还不是线程安全的。
出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。
解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:
publicclass NameList { private List nameList = Collections.synchronizedList(new LinkedList()); publicsynchronizedvoid add(String name) { nameList.add(name); } publicsynchronized String removeFirst() { if (nameList.size() > 0) { return (String) nameList.remove(0); } else { returnnull; } } }
这样,当一个线程访问其中一个同步方法时,其他线程只有等待。
线程的通信
wait() 与 notify() 和 notifyAll()
wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常
生产者消费者例子:
/** * Java线程:并发协作-生产者消费者模型 * */ publicclass Test { publicstaticvoid main(String[] args) { Godown godown = new Godown(30); Consumer c1 = new Consumer(50, godown); Consumer c2 = new Consumer(20, godown); Consumer c3 = new Consumer(30, godown); Producer p1 = new Producer(10, godown); Producer p2 = new Producer(10, godown); Producer p3 = new Producer(10, godown); Producer p4 = new Producer(10, godown); Producer p5 = new Producer(10, godown); Producer p6 = new Producer(10, godown); Producer p7 = new Producer(80, godown); c1.start(); c2.start(); c3.start(); p1.start(); p2.start(); p3.start(); p4.start(); p5.start(); p6.start(); p7.start(); } } /** * 仓库 */ class Godown { publicstaticfinalint max_size = 100;//最大库存量 publicint curnum; //当前库存量 Godown() { } Godown(int curnum) { this.curnum = curnum; } /** * 生产指定数量的产品 * * @param neednum */ public synchronized void produce(int neednum) { //测试是否需要生产 while (neednum + curnum > max_size) { System.out.println("要生产的产品数量" + neednum +"超过剩余库存量" + (max_size - curnum) +",暂时不能执行生产任务!"); try { //当前的生产线程等待 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //满足生产条件,则进行生产,这里简单的更改当前库存量 curnum += neednum; System.out.println("已经生产了" + neednum +"个产品,现仓储量为" + curnum); //唤醒在此对象监视器上等待的所有线程 notifyAll(); } /** * 消费指定数量的产品 * * @param neednum */ public synchronized void consume(int neednum) { //测试是否可消费 while (curnum < neednum) { try { //当前的生产线程等待 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //满足消费条件,则进行消费,这里简单的更改当前库存量 curnum -= neednum; System.out.println("已经消费了" + neednum +"个产品,现仓储量为" + curnum); //唤醒在此对象监视器上等待的所有线程 notifyAll(); } } /** * 生产者 */ class Producer extends Thread { privateint neednum; //生产产品的数量 private Godown godown; //仓库 Producer(int neednum, Godown godown) { this.neednum = neednum; this.godown = godown; } publicvoid run() { //生产指定数量的产品 godown.produce(neednum); } } /** * 消费者 */ class Consumer extends Thread { privateint neednum; //生产产品的数量 private Godown godown; //仓库 Consumer(int neednum, Godown godown) { this.neednum = neednum; this.godown = godown; } publicvoid run() { //消费指定数量的产品 godown.consume(neednum); } }
已经生产了10个产品,现仓储量为40 已经生产了10个产品,现仓储量为50 已经消费了50个产品,现仓储量为0 已经生产了80个产品,现仓储量为80 已经消费了30个产品,现仓储量为50 已经生产了10个产品,现仓储量为60 已经消费了20个产品,现仓储量为40 已经生产了10个产品,现仓储量为50 已经生产了10个产品,现仓储量为60 已经生产了10个产品,现仓储量为70 Process finished with exit code 0
说明:
对于本例,要说明的是当发现不能满足生产或者消费条件的时候,调用对象的wait方法,wait方法的作用是释放当前线程的所获得的锁,并调用对象的notifyAll()方法,通知(唤醒)该对象上其他等待线程,使得其继续执行。这样,整个生产者、消费者线程得以正确的协作执行。
notifyAll() 方法,起到的是一个通知作用,不释放锁,也不获取锁。只是告诉该对象上等待的线程“可以竞争执行了,都醒来去执行吧”。
本例仅仅是生产者消费者模型中最简单的一种表示,本例中,如果消费者消费的仓储量达不到满足,而又没有生产者,则程序会一直处于等待状态,这当然是不对的。实际上可以将此例进行修改,修改为,根据消费驱动生产,同时生产兼顾仓库,如果仓不满就生产,并对每次最大消费量做个限制,这样就不存在此问题了,当然这样的例子更复杂,更难以说明这样一个简单模型。
总结: 释放锁: wait();
不释放锁:sleep() yield() suspend() (过时,可能导致死锁)
线程死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
死锁是我们在使用同步时,需要避免的问题!
//死锁的问题:处理线程同步时容易出现。
//不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
//写代码时,要避免死锁!
public class TestDeadLock { static StringBuffer sb1 = new StringBuffer(); static StringBuffer sb2 = new StringBuffer(); public static void main(String[] args) { new Thread() { public void run() { synchronized (sb1) { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } sb1.append("A"); synchronized (sb2) { sb2.append("B"); System.out.println(sb1); System.out.println(sb2); } } } }.start(); new Thread() { public void run() { synchronized (sb2) { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } sb1.append("C"); synchronized (sb1) { sb2.append("D"); System.out.println(sb1); System.out.println(sb2); } } } }.start(); } }
线程的调度-守护线程
守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
setDaemon方法的详细说明:
publicfinalvoid setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。 参数: on - 如果为true,则将该线程标记为守护线程。 抛出: IllegalThreadStateException - 如果该线程处于活动状态。 SecurityException - 如果当前线程无法修改该线程。 另请参见: isDaemon(), checkAccess()
```java ```java ```java /** * Java线程:线程的调度-守护线程 * * @author leizhimin 2021-1-6 9:02:40 */ public class Test { public static void main(String[] args) { Thread t1 = new MyCommon(); Thread t2 = new Thread(new MyDaemon()); t2.setDaemon(true);//设置为守护线程 t2.start(); t1.start(); } } class MyCommon extends Thread { publicvoid run() { for (int i = 0; i < 5; i++) { System.out.println("线程1第" + i + "次执行!"); try { Thread.sleep(7); } catch (InterruptedException e){ e.printStackTrace(); } } } } class MyDaemon implements Runnable { publicvoid run() { for (long i = 0; i < 9999999L; i++) { System.out.println("后台线程第" + i +"次执行!"); try { Thread.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } }
```java 后台线程第0次执行! 线程1第0次执行! 线程1第1次执行! 后台线程第1次执行! 后台线程第2次执行! 线程1第2次执行! 线程1第3次执行! 后台线程第3次执行! 线程1第4次执行! 后台线程第4次执行! 后台线程第5次执行! 后台线程第6次执行! 后台线程第7次执行! Process finished with exit code 0
从上面的执行结果可以看出:
前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。
实际上:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。
Java线程:volatile关键字
Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。
谈及到volatile关键字,不得不提的一篇文章是:《Java理论与实践:正确使用 Volatile 变量》,这篇文章对volatile关键字的用法做了相当精辟的阐述。
之所以要单独提出volatile这个不常用的关键字原因是这个关键字在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题。
首先考虑一个问题,为什么变量需要volatile来修饰呢?
要搞清楚这个问题,首先应该明白计算机内部都做什么了。比如做了一个i++操作,计算机内部做了三次处理:读取-修改-写入。
同样,对于一个long型数据,做了个赋值操作,在32系统下需要经过两步才能完成,先修改低32位,然后修改高32位。
假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题,是这些步骤执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。
通过这个设想,就不难理解volatile关键字了。
volatile可以用在任何变量前面,但不能用于final变量前面,因为final型的变量是禁止修改的。也不存在线程安全的问题。