1.1、多线程基本使用
1、线程的创建方式
多线程的创建有两种方式,分别如下:
继承
- 继承Thread类,并重写run方法,将需要多线程的代码放入run方法中。
- 通过Thread的子类的引用调用start()方法来开启线程。
实现
- 定义类实现Runnable接口,覆盖Runnable接口中的run方法。
- 通过Thread类建立线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
- 调用Thread类的star方法开启线程并调用Runnable接口子类的run方法。
继承的方式:
public class ThreadDemo { public static void main(String[] args) { new MyThread().start(); } } class MyThread extends Thread{ @Override public void run() { // 多线程代码 System.out.println("多线程开启了"); } }
实现的方式:
public class ThreadDemo { public static void main(String[] args) { // 第一种写法 /* MyThread t1 = new MyThread(); Thread thread = new Thread(t1); thread.start();*/ // 开发中写法 new Thread(){ @Override public void run() { System.out.println("线程开启了"); } }.start(); } } class MyThread implements Runnable{ public void run() { System.out.println("线程开启了"); } }
线程都有自己默认的名称,通过getName和setName来进行获取和设置。同时,还可以通过Thread.currentThread()来获取当前线程对象。
2、 线程的运行状态
被创建-------------------->运行-------------------------->消亡(stop(已过时)或run方法结束)
|
阻塞-->(阻塞状态:具备运行资格,但不具备cpu执行权。)
|
冻结(sleep和wait):不具备运行资格,也不具备cpu执行权。
睡眠状态和等待状态:不具备运行资格,也没有执行权。
- 睡眠状态:sleep(time),当线程遇到sleep会进入睡眠状态,当睡眠时间到达后可能是运行状态,也可能进入临时状态(阻塞状态)。
- 等待状态:wait(),当线程遇到wait会进入等待状态,这时需要notify()来唤醒,唤醒后可能是运行状态,也可能是临时状态(阻塞状态)。
消亡的两种方式:通过stop()命令强行结束线程或run()方法执行结束。
3、 多线程售票实例
火车站有100张票,分别在t1、t2、t3、t4等4个窗口进行出售。
由此首先我们考虑到的是将100张票定义为静态共享变量,并开启四个线程来出售,那么代码如下:
public class ThreadDemo { public static void main(String[] args) { Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket);// 售票窗1 Thread t2 = new Thread(ticket);// 售票窗2 Thread t3 = new Thread(ticket);// 售票窗3 Thread t4 = new Thread(ticket);// 售票窗4 t1.start(); t2.start(); t3.start(); t4.start(); } } // 售票厅 class Ticket extends Thread { private static int ticks = 100;// 出售票的方法 public void sale() { System.out.println(Thread.currentThread().getName() + "sale:" + ticks--); } @Override public void run() { while (true) { if (ticks > 0) { sale(); } } } }
输出结果:
Thread-0sale:3Thread-0sale:2Thread-0sale:1Thread-2sale:93Thread-1sale:89Thread-3sale:91
从结果中我们可以看出,售卖过程中我们发现出售的顺序已经错乱,这是因为线程太快导致,我们可以让线程在输出时进行睡眠20毫秒:
// 售票厅 class Ticket extends Thread{ private static int ticks = 100; // 出售票的方法 public void sale(){ System.out.println(Thread.currentThread().getName() + "sale:" + ticks--); } @Override public void run() { while(true){ if(ticks > 0){ try{Thread.sleep(20);}catch(Exception e){} sale(); } } } }
输出结果:
Thread-2sale:77 Thread-3sale:77 Thread-0sale:77 Thread-1sale:77
从结果中,我们看到出现售卖相同的票,那么线程的安全隐患就暴露出来了,那么我们如何解决这个问题呢?
我们只能通过线程同步来解决该问题,来保住每次被并发执行的代码中只能有一个线程在操作,以此解决线程的安全问题。
4、 多线程同步
由上面代码的示例我们知道,直接开启四个线程进行售卖是存在安全隐患的,我们必须通过同步来解决线程安全问题。
同步的前提:
- 必须要有两个或者两个以上的线程。
- 必须是多个线程使用同一个锁。必须保证同步中只能有一个线程在运行。
线程的同步解决了多线程的安全问题,但是多个线程都需要判断锁,较为耗费资源。
a) 如果需要同步的代码只是部分,则可以使用同步代码块,同步代码块的锁对象可以是任意的对象:
// 售票厅 class Ticket extends Thread { private static int ticks = 100; Object obj = new Object(); // 出售票的方法 public void sale() { System.out.println(Thread.currentThread().getName() + "sale:" + ticks--); } @Override public void run() { while (true) { synchronized(obj){ if (ticks > 0) { try{Thread.sleep(20);}catch(Exception e){} sale(); } } } } }
输出结果:
Thread-1sale:100 Thread-1sale:99 Thread-1sale:98 ... Thread-2sale:3 Thread-2sale:2 Thread-2sale:1
b) 函数需要被对象调用。那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。
// 售票厅 class Ticket extends Thread { private static int ticks = 100; Object obj = new Object(); // 出售票的方法 public void sale() { System.out.println(Thread.currentThread().getName() + "sale:" + ticks--); } @Override public void run() { while (true) { synSal(); } } // 同步函数 public synchronized void synSal(){ if (ticks > 0) { try{Thread.sleep(20);}catch(Exception e){} sale(); } } }
输出结果:
Thread-1sale:100 Thread-1sale:99 Thread-1sale:98 ... Thread-2sale:3 Thread-2sale:2 Thread-2sale:1
c) 当同步函数被static(静态)所修饰的时候,使用的锁是所在类的字节码文件(类名.class)。
// 售票厅 class Ticket extends Thread { private static int ticks = 100; Object obj = new Object(); // 出售票的方法 public static void sale() { System.out.println(Thread.currentThread().getName() + "sale:" + ticks--); } @Override public void run() { while (true) { synSal(); } } // 同步函数 public static synchronized void synSal(){ if (ticks > 0) { try{Thread.sleep(20);}catch(Exception e){} sale(); } } }
输出结果:
Thread-1sale:100 Thread-1sale:99 Thread-1sale:98 ... Thread-2sale:3 Thread-2sale:2 Thread-2sale:1
d) 多线程--死锁,同步中嵌套同步,而锁却不同就会造成死锁,从而导致程序无法继续向下运行。
class DeadLock implements Runnable{ //定义一个标记 private boolean flag; DeadLock(boolean flag){ this.flag = flag; } //重写run方法 public void run(){ if(flag){ synchronized(MyLock.locka){ synchronized(MyLock.lockb){ System.out.println("if locka"); } } } else{ synchronized(MyLock.lockb){ synchronized(MyLock.locka){ System.out.println("else lockb"); } } } } } //定义两个锁 class MyLock{ static Object locka = new Object(); static Object lockb = new Object(); } public class DeadLockDemo { public static void main(String[] args) { new Thread(new DeadLock(true)).start(); new Thread(new DeadLock(false)).start(); } }
5、多线程之间的通信
a) 等待唤醒机制
- notify():激活线程池中 wait的线程。(谁先进去就唤醒谁)
- notifyAll():激活线程池中所有wait的线程。
- notify,notifyAll和wait等方法必须用在同步中,也就是说同步是前提。
- 而且这几种方法都是继承自Object类,出现异常,只能try而不能抛。
- 等待和唤醒必须是同一个锁,锁可以是任意对象,所以方法定义在Object类中。
- 都使用在同步中,因为要持有监视器(锁)的线程操作。
- 所以要使用在同步中,因为只有同步才具有锁。
b) 生产者和消费者实例
我们来看一个生产者和消费者的实例,在生产商品的同时将商品销售或消费掉。
class ProducerConsumerDemo { public static void main(String[] args) { Resource res = new Resource(); Producer pro = new Producer(res); Consumer con = new Consumer(res); Thread t1 = new Thread(pro); Thread t2 = new Thread(pro); Thread t3 = new Thread(con); Thread t4 = new Thread(con); t1.start(); t2.start(); t3.start(); t4.start(); } } class Resource { private String name; private int count = 1; private boolean flag = false; public synchronized void set(String name) { while (flag) try { this.wait(); } catch (Exception e) { } this.name = name + "--" + count++; System.out.println(Thread.currentThread().getName() + "...生产者.." + this.name); flag = true; this.notifyAll(); } public synchronized void out() { while (!flag) try { wait(); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name); flag = false; this.notifyAll(); } } class Producer implements Runnable { private Resource res; Producer(Resource res) { this.res = res; } public void run() { while (true) { res.set("+商品+"); } } } class Consumer implements Runnable { private Resource res; Consumer(Resource res) { this.res = res; } public void run() { while (true) { res.out(); } } }
为什么要定义while判断标记?
对于多个生产者和消费者需要用while判断标记,来让被唤醒的线程再一次判断标记。
为什么要定义notifyAll()?
因为在本线程进入冻结或睡眠状态时,需要唤醒对方线程,如果用notify(),容易出现只唤醒本方线程的情况,导致程序中所有线程都进入等待状态。
我们可以看下简单的总结:
一个生产线程对应一个消费线程,我们可以用if(flag)来判断标记,唤醒必须用notify()。
if(flag) --> notify()
多个生产线程对应多个消费线程,我们必须用while(flag)来判断标记,唤醒必须用notifyAll()。(while循环下唤醒是notify()的话会导致全部等待)
while(flag) --> notifyAll();
死锁和全部等待:死锁是争抢执行权而导致程序停止,全部等待是所有线程都进入冻结状态。
针对唤醒本方的同时也唤醒对方的问题,Jdk1.5对该问题进行了针对性的处理,请参考下面的新特性。