03 多线程
多线程实现方法
使用 实现 Runnable 接口的方式, 实现了 Runnable 的类的实例由线程创建. 必须实现 run 方法.
用户线程: 默认情况下, 线程都是用户线程
守护线程: 用来守护用户线程的.
run 这种多线程的方法: (假装 Employee 实现了 runnable 方法
Employee emp = new Employee();
new Thread(emp).start(); // 交给 CPU 去调用, CPU 具体什么时候执行, 我们不知道. 等时间片轮转, 并不是直接执行run(), start 才是开启新的线程, start 会自己调用 run 方法.
package com.My.Thread; public class My12306 implements Runnable { private int ticketNum = 99; public static void main(String[] args) { new Thread(new My12306(), "one").start(); new Thread(new My12306(), "two").start(); new Thread(new My12306(), "three").start(); } @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } while (ticketNum > 0) { System.out.println(Thread.currentThread().getName() + "-->" + ticketNum--); } } }
龟兔赛跑
package com.My.Thread; public class MyRace implements Runnable { // 这块必须使用 static 生命, 这样多个线程之间才使用的是同一个 winner private static String winner; public static void main(String[] args) { new Thread(new MyRace(), "rabbit").start(); new Thread(new MyRace(), "tortoise").start(); } @Override public void run() { for (int step=1; step<=100; step++) { System.out.println(Thread.currentThread().getName() + "==>" + step); if (gameOver(step)) { break; } else { if(Thread.currentThread().getName().equals("rabbit") && step % 10 == 0) { try { Thread.sleep(2); // 兔子总睡觉 } catch (InterruptedException e) { e.printStackTrace(); } } } } } public boolean gameOver(int step) { if (null != winner) { return true; } else if (100 == step) { winner = Thread.currentThread().getName(); System.out.println("WInner is " + winner); return true; } return false; } }
线程状态
进入就绪状态, 4种方法:
1) start
2) 阻塞事件解除 -> 回到就绪状态
3) yield 方法, 让出CPU, 那么本线程进入就绪状态, Thread.yield(), 本线程谦让出CPU之后立刻进入就绪状态,
所以很有可能下一个时间片它又再次获得 CPU 来进入运行状态.
4) JVM 本身将CPU 从本线程切换到其他线程
进入阻塞状态, 4种方法:
1) sleep, 该线程继续占用对象资源, Thread.sleep(200)
2) wait, 本线程让出 CPU 和 对象资源
3) join, 插队,a 线程调用join, a.join(), a.join()这个语句被写到哪个b线程里, b线程被阻塞, 而且要b等到a线程本身执行完毕, b线程才能继续执行.
4) I/O 操作, read, write, 必须通过操作系统去调度
运行状态 : 当就绪状态的线程被 CPU 执行. 注意必须是从就绪状态来的. 阻塞状态不能直接执行.
死亡状态:
1) 线程自己运行完.
2) 当标记执行完的 flag 被外部触发, 线程执行完毕. 举例如下:
package com.My.Thread; public class ControlTerminateThread implements Runnable { private boolean flag = true; private String name; public ControlTerminateThread(String name) { super(); this.name = name; } @Override public void run() { int i = 0; while (flag) { System.out.println(this.name + "-->" + i++); } } public void terminate() { this.flag = false; System.out.println(this.name + "dead"); } public static void main(String[] args) { ControlTerminateThread cl = new ControlTerminateThread("C罗"); new Thread(cl).start(); for (int i = 0; i < 100; i++) { if (i == 88) { cl.terminate(); } System.out.println("main" + "-->" + i); } } }
线程的优先级就是获得 CPU 的概率, 默认是 5, 范围: 1-10
getPriority(), setPriority(MIN_PRIORITY)
线程分类
1. 用户线程(默认的)
2.守护线程:为用户线程服务,JVM停止不用等待守护线程结束
Thread t = new Thread(god); // god 是一个实例(当然类是实现了Runnable接口的)
t.setDaemon(true) // 将这个线程设置成守护线程, 注意一定要在t.start()之前来设置
t.start();
常用的方法
线程同步 / 线程安全 Synchronized
如果线程本身只是读一个对象, 没有改的操作, 那么不用保证线程安全.
并发: 多个线程同时操作同一个对象.
保证线程安全, 排队, 对资源(对象)加锁. 每个对象本身就有一个锁, 当线程想操作对象时,需要先获得这个锁.
锁谁?
锁对象:
给你一个对象让你锁定. (同步块)
锁 this (如果你操作的是一个成员方法, 那 this 就是当前对象本身, 如果是一个静态方法, 那 this 就是指类模板本身), 锁的最小单位就是对象,
所以说, 如果没有特殊给出要锁谁, 只是单纯的在方法前加 synchronized, 那就是在锁你当前这个方法的对象.
并且, 注意,一定要锁住修改数据(共享资源) 的代码.
同步方法: 在方法之前加入 synchronized 就可以, 因为我们的对象的数据都是在方法中操作的,所以在方法前加 sychronized, 该方法在执行时,
这就相当于锁 this, 但是, 当你的这个方法体中, 有其他对象的实例变量时, 它没有被锁, 这样可能会导致问题. 解决的办法是用数据块枷锁.
同步块: 个人推荐这种方法加锁, 一方面被加锁的代码范围小, 而且更加显示的说明了加锁的对象.
双重检测, 多线程
如果ticketNum 已经被修改成 0 了, 也就是没票了, 那么实际上, 这时就不用再监控对象来加锁了, 因为这时状态已经确定了.
但是在同步块内部, 放上相同的语句, 主要是用来监视最后 1 张票, (个人感觉影响性能不大)
上边的 if (account.money <= 0) 判断非常重要, 我们应该在程序进入同步块之前尽量的避免进入, 因为它非常影响性能.
而且不要忘了, 多线程环境下, 主程序本身也是线程之一, 它也仅仅跟其他线程是一样的.
两种锁举例: 用 12306 举例:
package com.My.Thread; public class My12306 { public static void main(String[] args) { TicketControl tc = new TicketControl(); new Thread(tc, "one").start(); new Thread(tc, "two").start(); new Thread(tc, "three").start(); } } class TicketControl implements Runnable { private int ticketNum = 99; private boolean flag = true; @Override public void run() { while (flag) { // test1(); test2(); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void test1() { if (ticketNum <= 0) { flag = false; return; } else { System.out.println(Thread.currentThread().getName() + "-->" + ticketNum--); } } public void test2() { if (ticketNum <= 0) { flag = false; return; } synchronized(this) { if (ticketNum <= 0) { flag = false; return; } System.out.println(Thread.currentThread().getName() + "-->" + ticketNum--); } } }
举例: 电影院
package com.My.Thread; public class HappyCinema { public static void main(String[] args) { Cinema ci = new Cinema(20); String[] arrayName = {"a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9"}; for(String name: arrayName) { new Thread(new Consumuer(ci, 2), name).start(); } } } class Cinema{ private int avaliable; public Cinema(int avaliable) { super(); this.avaliable = avaliable; } public boolean bookTicket(int seat) { System.out.println("可用位置为:" + avaliable); if(this.avaliable < seat) { System.out.println("购票失败, 票不够了"); return false; } this.avaliable -= seat; System.out.println("购票成功, 当前还剩:" + this.avaliable); return true; } } class Consumuer implements Runnable { Cinema cinema; private int seat; public Consumuer(Cinema cinema, int seat) { super(); this.cinema = cinema; this.seat = seat; } @Override public void run() { getTicket2(); } public synchronized void getTicket() { if (cinema.bookTicket(seat)){ System.out.println(Thread.currentThread().getName() + ": getTicket"); } else { System.out.println(Thread.currentThread().getName() + ": don't get"); }; try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } public void getTicket2() { synchronized(cinema) { if (cinema.bookTicket(seat)){ System.out.println(Thread.currentThread().getName() + ": getTicket"); } else { System.out.println(Thread.currentThread().getName() + ": don't get"); }; } try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } }
注意, 因为加锁的是对象, 所以类似 new Thread(new ObjectLock1()).start(), new Thread(new ObjectLock2()).start() 类似这种写法, 对加锁是没有意义的,
因为 ObjectLock1 和 ObjectLock2 本身就是2个对象,他们之间的成员变量是没有交集的,所以正确的写法应该是:
ObjectLock ol = new ObjectLock(); new Thread(ol, "one").start(); new Thread(ol, "two").start();
这样, 这两个线程之间才可能去访问同一个对象的成员变量, 才需要加锁.
死锁
当一个同步块中有多个对象的锁, a 线程的同步块获得了对象 X 的锁, b 线程的同步块获得了对象 Y 的锁, 同时,a 想申请 Y 锁, b 想申请 X 锁.
解决办法: 不要在同一个同步块中出现多个对象锁, 比如
synchronized(X) { // 表示获得了 X 锁
synchronized(Y) // 再其内部又想去获得 Y 锁, 不要这样. 保持同步块只锁一个对象, 把这个代码拿到 synchronized(X)的外边去.
}
正确的方式:
synchronized(X) { 修改与对象 X 有关的资源 }
synchronized(Y) { 修改与对象 Y 有关的资源 }
生产者消费者模型
生产者 和 消费者都是多线程, 显然中间的容器需要并发. 生产者存, 消费者取
wait() 进入等待状态(排队), 需要等待, 直到另一个线程调用该对象本身的 notify() / notifyAll() 来进行唤醒。
notify(): 唤醒正在等待对象监视器的单个线程.
notifyAll(): 唤醒所有正在等待对象监视器的所有线程.
首先, 假如说代码(class)是 x, a, b 是两个线程, 那么, a 和 b 都是按照 class 这个模子出来的线程, 现在假如在 a 和 b 内部分别调用了 this.wait(), 这时, a 和 b 这两个线程都进入了无条件长时间等待状态(释放自己占用的锁), 如果 a 线程内部有其他方法运行, 并且调用了 this.notify(), 注意这个方法在 a 线程内部, 所以这里的this实际上是指向a的,所以a 从等待状态出来, 进入排队, 准备加锁然后运行, 但是, 如果还是在线程 a 的内部, 代码是 this.notifyAll(), 表示,由这个模子刻出来的所有线程(本例中就是 a 和 b), 都可以从等待状态出来了, 进入排队状态, 然后 a 和 b 都等着获取资源(锁), 然后执行. 所以可以看到, 实际上从 wait 状态返回时, 进入等待队列(实际上也近似可以理解为进入就绪状态).
缓冲区法:
package com.My.Thread; public class MyProCum { public static void main(String[] args) { // container 是需要加锁的资源 ContainerPool container = new ContainerPool(); // product & consumer 是可以通过并发启动的 Produce product = new Produce(container); Consumer consumer = new Consumer(container); new Thread(product).start(); new Thread(consumer).start(); } } //生产者 class Produce implements Runnable { ContainerPool con; public Produce(ContainerPool con) { super(); this.con = con; } public void doProduce() { for (int i = 0; i < 100; i++) { System.out.println("生产 -->" +i+ " salt"); con.push(new Salt(i)); } } @Override public void run() { doProduce(); } } //消费者 class Consumer implements Runnable { ContainerPool con; public Consumer(ContainerPool con) { super(); this.con = con; } public void doConsum() { for (int i = 0; i < 100; i++) { System.out.println("消费 --> "+ con.pop().getId() +" Salt"); } } @Override public void run() { doConsum(); } } //资源池 class ContainerPool { Salt[] salt = new Salt[10]; // 需要加锁的资源 int count = 0; // 记录当前资源池的用量的栈顶 //资源入池 public synchronized void push(Salt s) { // 资源池满, 进入等待 if (count == salt.length) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 资源池有空间 salt[count] = s; count++; this.notifyAll(); // 释放了消费者, 同时也释放了其他生产者, 因为别人也可以生产 } //资源出池 public synchronized Salt pop() { // 资源池空, 等待 if (count == 0) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 每次都从资源池的最上边出 count--; Salt s = salt[count]; this.notifyAll(); // 释放了生产者, 同时也释放了其他消费者, 因为有资源了,其他人也可以消费 return s; } } //资源本身 Salt class Salt { private int id; public Salt(int id) { super(); this.id = id; } public int getId() { return id; } public void setId(int id) { this.id = id; } }
信号灯: 借助标志位的真假来判定
package com.My.Thread; public class MySingleThread { public static void main(String[] args) { MyTY tv = new MyTY(); Player p = new Player(tv); Listener l = new Listener(tv); new Thread(p).start(); new Thread(l).start(); } } // 生产者(演员), 多线程 class Player implements Runnable { MyTY tv; public Player(MyTY tv) { super(); this.tv = tv; } @Override public void run() { startPlay(); } public void startPlay() { for (int i = 0; i < 20; i++) { if (0 == i%2) { tv.play("Sing"); } else { tv.play("Dance"); } } } } // 消费者(观众), 多线程 class Listener implements Runnable { MyTY tv; public Listener(MyTY tv) { super(); this.tv = tv; } @Override public void run() { startWatch(); } public void startWatch() { for (int i = 0; i < 20; i++) { // 这块必须是 20 因为前面表演了20个节目, 也必须看20个, 否则标记为不匹配 tv.watch(); } } } // 信号灯资源, 并发资源 class MyTY { // 如果 flag 为真, 表示当前已经有演出了, 可以消费, 但不可以生产 // 如果 flag 为假, 表示当前没有演出了, 可以produce, 但不可以消费 private boolean flag = false; String voice; // 表演节目 public synchronized void play(String voice) { // 如果是演员等待. if (flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 不是等待, 就执行. 注意这里不能写到 else 里. 因为当从 wait唤醒时, 还有可能继续执行这个函数的代码. System.out.println("表演了(生产)--> " + voice + " " + flag); this.voice = voice; flag = !flag; this.notifyAll(); // 因为已经生产, 所以释放消费 } // 收听节目 public synchronized void watch() { // 如果是观众等待, 就进入等待 if (!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("收听到了(消费)--> " + voice + " " + flag); flag = !flag; this.notifyAll(); // 因为已经消费, 所以释放生产 } }
特别注意:
wait () 这种, 函数一定要这样写:
function() {
wait() // 触发wait
真正函数体. 这里不能把真正函数体作为wait()的对立面分支, 比如 if true -> wait, if false -> 执行代码. 这样肯定不行, 因为wait被唤醒时, 程序要继续执行, 而你把程序写成了对立分支之后, 即便wait被唤醒了,程序本身也没什么可执行的了(因为对立分支肯定走不到)
}
高级主题
Timer & QUARTZ
如果比较复杂的时间调度, 使用 QUARTZ 来调用. (需要 VPN 下载)
定时任务, Timer 和 TimerTask, 一般都是 timer.schedule(new TimerTask) 类似这种任务调度. 具体的查询 API
package com.My.Thread; import java.util.Timer; import java.util.TimerTask; public class MyTimerTest { public static void main(String[] args) { Timer t = new Timer(); t.schedule(new MyTask(), 1000); } } class MyTask extends TimerTask { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("Hello world"); } } }
ThreadLocal
悲观锁 & 乐观锁
利用比较并交换实现 乐观锁, 乐观锁实际上没有给资源加锁, 而是在修改完成判断, 比如当前资源的版本 1 和 线程本身修改时保存的那个版本是否一致, 如果一致,证明没有线程修改过这个资源(因为修改资源后, 要把版本设置成2),这时修改成功, 然后将版本设置成2.
B如资源:
sugar 和 version, 每次线程要修改时, 将这两个变量值读入线程内部, 修改, 然后提交时, 判断当前线程保存的这个version 和 公共内存中的那个version是否一致, 如果一致(证明没有人修改过该资源),对资源进行修改,并将version 改成2. 但是这里有一个问题,因为没有给资源加锁, 所以, 如果有线程刚好做判断过后的时候对公共内存的 version 和 资源进行了修改并生效, 那么,这样就会有问题. 所以, 乐观锁是不可靠的.
比较并交换,有通过 version, 还有通过原始值来判断的,通过原始值判断的, 可能就会有 A -> B -> A 的问题, 但是通过递增的 version,就不会有这种情况.