提纲:
一.开篇废话
二.再看多线程
1)线程的状态(这一部分是总结前面所学的线程基础知识)
2)线程的同步问题(这一部分是最近的一些收获)
一.开篇废话
Where又回来了,貌似有快一个学期没写技术博客了,虽然期间做过一些总结但也没发上博客,又到了假期可以安下心好好写点东西了,废话不多说,希望自己在寒假能安心下来好好学点东西,写点东西,那么让我们从线程开始。
二.再看线程
1.线程的几个状态
一个线程可以有四种状态:
(1) 新(New):线程对象已经创建,但尚未启动,所以不可运行。
问题一:如何新建线程?
答:让一个类成为线程类的方式有两种:一个是实现java.lang.Runnable接口,另一个是继承java.lang.Thread类。
范例代码如下:
public class TestThread extends Thread {
//继承Thread类的线程类
public void run(){
System.out.println("线程开始运行");
do...
}
}
public class TestThread1 implements Runnable{
//实现Runable接口的线程类
public void run(){
System.out.println("线程开始运行");
do...
}
}
问题二:比较这两种方式的异同?
答:
相同点:两者都需要根据具体要求实现run()方法。
区别:(1)线程类继承自Thread类则不能继承其他类,而Runnable可以。(因为JAVA的类只能继承自一个类,却可以实现多个接口,这个在之前的博客有提到)。
(2)线程类继承Thread类相对于Runnable来说,使用线程的方法更方便些。(因为Thread类提供了很多关于线程的方法,比如:获取线程名,线程状态等)。
(3) 实现Runable接口的线程类的多个线程,可以更方便的访问同一变量可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的
情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。,而Thread类则需要内部类替换。
如果发现第三点不好理解那就看一下我下面的例子:
现有火车票10张(1-10号),现有X和Y两个代售点要把这十张票卖出我们用Thread和Runnable来试验一下这个过程。
package 比较多线程实现方法;
/**
* 用Runnable的方式模拟场景
* @author where
*
*/
public class TestRunnable implements Runnable{
private static int MAX=10;//共有MAX张票
public void run() {
while(MAX>0){
System.out.println("-"+Thread.currentThread().getName()+"-卖掉第 : "+MAX--+" 张");
}
}
public static void main(String[] args) {
TestRunnable tt0=new TestRunnable();
new Thread(tt0,"X").start();
TestRunnable tt1=new TestRunnable();
new Thread(tt1,"Y").start();
System.out.println("-------");
}
}
运行结果:
-X-卖掉第 : 9 张
-X-卖掉第 : 8 张
-X-卖掉第 : 7 张
-Y-卖掉第 : 10 张
-Y-卖掉第 : 5 张
-X-卖掉第 : 6 张
-Y-卖掉第 : 4 张
-X-卖掉第 : 3 张
-Y-卖掉第 : 2 张
-X-卖掉第 : 1 张
可已看出用这种方式的结果是对的,那么我们再来看看另一种。
package 比较多线程实现方法;
/**
* 用Thread方式来模拟场景
* @author where
*
*/
public class TestThread extends Thread{
private static int MAX=10;//共有MAX张票
public void run() {
while(MAX>0){
System.out.println("-"+Thread.currentThread().getName()+"-卖掉第 : "+MAX--+" 张");
}
}
public static void main(String[] args) {
TestRunnable tt0=new TestRunnable();
new Thread(tt0,"X").start();
TestRunnable tt1=new TestRunnable();
new Thread(tt1,"Y").start();
System.out.println("-------");
}
}
运行结果(这只是结果的一种情况):
-Y-卖掉第 : 10 张
-X-卖掉第 : 10 张
-Y-卖掉第 : 9 张
-Y-卖掉第 : 7 张
-X-卖掉第 : 8 张
-Y-卖掉第 : 6 张
-Y-卖掉第 : 4 张
-X-卖掉第 : 5 张
- X-卖掉第 : 2 张
-X-卖掉第 : 1 张
-Y-卖掉第 : 3 张
这次就有问题了,Y售票站已经将10号票售出为什么X又买出了一张10号票,这就是上面我们讲的那个问题。貌似到这里你该明白第三点了吧。
(2) 可运行(Runnable ):意味着一旦时间分片机制有空闲的CPU 周期提供给一个线程,那个线程便可立即
开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既
没有“死”掉,也未被“堵塞”。
(3) 死(Dead):从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用stop()令其死掉,但会
产生一个违例——属于Error 的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当
是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用stop()(在Java 1.2 则是坚决反
对)。另外还有一个destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根
本不会解除对象的锁定。
(4) 堵塞(Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地
跳过它,不给它分配任何CPU 时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。
问题三:为何会堵塞?
答:堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造成的:
1.调用sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。
2. 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回“可运行”状态。
3. 用wait()暂停了线程的执行。除非线程收到nofify()或者notifyAll()消息,否则不会变成“可运行”
(是的,这看起来同原因2 非常相象,但有一个明显的区别是我们马上要揭示的)。
4. 线程正在等候一些IO(输入输出)操作完成。
5. 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。
2.线程的同步问题(这一部分是最近的一些收获)
多线程一旦操作同一内存的数据就容易造成数据的混乱,也就是常说的线程安全问题。针对这个问题我们一般有四种解决方式:
- 采用synchronized关键字锁定要同步的方法
- 采用synchronized关键字锁定要同步的代码块
- 采用lock锁对象锁定同步代码,注意这里的所对象必须要为同一个否则加锁没意义。
- 采用共享的代码操作为原子操作
- 在这一部分我用生产者消费者模型为例说明:场景模拟:有一个最大存货量为5的仓库,生产者往里放,消费者取,当放满了则生产者wait,当取空了则消费者wait.
package 生产消费者模型; //用wait/notify public class Store { public static final int MAXSIZE = 5;//表示仓库里的最大存货量 int n;//表示仓库里当时的存货量 public Store(int n){ this.n= n; } /** * 放入货物的方法 */ public synchronized void addThing(){ while(n>=MAXSIZE){ try { System.out.println(Thread.currentThread().getName()+"当前仓库已经存放到最大限度,需等待才能在放"); //notifyAll(); wait(); } catch (InterruptedException e) { e.printStackTrace(); } } n++; System.out.println(Thread.currentThread().getName()+"当前仓库放入了"+n+"个或物"); notify(); } /** * 移除货物的方法 */ public synchronized void removeThing(){ while(n<=0){ try { System.out.println(Thread.currentThread().getName()+"当前仓库已没有货物要取需等待"); //notifyAll(); wait(); } catch (InterruptedException e) { e.printStackTrace(); } } n--; System.out.println(Thread.currentThread().getName()+"仓库被取走货物后剩下的货物为:"+n+"个"); notify(); } }
package 生产消费者模型; public class Customer extends Thread{ Store s; public Customer(Store s){ this.s = s; } public void run(){ while(true){ s.removeThing(); try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
package 生产消费者模型; public class Producer extends Thread{ private Store s; public Producer(Store s){ this.s = s; } public void run(){ while(true){ s.addThing(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果(由于是while(true)所以只选取了部分结果来说明问题):package 生产消费者模型; public class Test { /** * 测试入口 */ public static void main(String[] args) { Store s = new Store(0); Thread pro1 = new Producer(s); Thread pro2 = new Producer(s); //Thread pro3 = new Producer(s); Thread cus1 = new Customer(s); Thread cus2 = new Customer(s); Thread cus3 = new Customer(s); pro1.start(); pro2.start(); //pro3.start(); cus1.start(); cus2.start(); cus3.start(); } }
Thread-0当前仓库放入了1个或物
Thread-2仓库被取走货物后剩下的货物为:0个
Thread-3当前仓库已没有货物要取需等待
Thread-1当前仓库放入了1个或物
Thread-3仓库被取走货物后剩下的货物为:0个
Thread-4当前仓库已没有货物要取需等待
Thread-0当前仓库放入了1个或物
Thread-4仓库被取走货物后剩下的货物为:0个
Thread-1当前仓库放入了1个或物
Thread-0当前仓库放入了2个或物
Thread-1当前仓库放入了3个或物
Thread-0当前仓库放入了4个或物
Thread-2仓库被取走货物后剩下的货物为:3个
Thread-3仓库被取走货物后剩下的货物为:2个
Thread-1当前仓库放入了3个或物
Thread-0当前仓库放入了4个或物
Thread-4仓库被取走货物后剩下的货物为:3个
Thread-1当前仓库放入了4个或物
Thread-0当前仓库放入了5个或物
Thread-1当前仓库已经存放到最大限度,需等待才能在放
Thread-0当前仓库已经存放到最大限度,需等待才能在放
Thread-2仓库被取走货物后剩下的货物为:4个
Thread-1当前仓库放入了5个或物
Thread-0当前仓库已经存放到最大限度,需等待才能在放
Thread-3仓库被取走货物后剩下的货物为:4个
Thread-0当前仓库放入了5个或物
...
结果分析:
(1)线程并不是按它们创建时的顺序运行的
(2)当执行到后面,有多个wait(),那么notify该激活哪个wait或者说它有什么调度机制?根据结果(标红的部分)我们可以做此大胆猜想:这里有一个wait队列,先进先出,即notify相应最先进队列的wait.