Java多线程
一、程序 进程 线程
- 程序:指令集 静态资源
- 进程:操作系统 调度程序 动态概念
- 线程:在进程内多条执行路径
二、创建
Java中线程的创建常见有如三种基本形式
1、继承Thread类,重写该类的run方法
兔子类:
public class Rabbit extends Thread{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println("兔子跑了"+i+"步");
}
}
}
乌龟类:
public class Tortoise extends Thread {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println("乌龟跑了"+i+"步");
}
}
}
龟兔赛跑:
public class Game {
public static void main(String[] args) {
Rabbit rabbit = new Rabbit();
Tortoise tortoise = new Tortoise();
rabbit.start();
tortoise.start();
}
}
2、实现Runnable接口,并重写该接口的run()方法。
该run()方法同样是线程执行体。
(1)步骤
- 创建Runnable实现类的实例。
- 并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
- Thread对象.start()
(2)优势
- 避免了单继承的局限性
- 便于共享资源 适合多个相同的程序代码的线程去处理同一个资源
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
public class Programmer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("一遍写代码一遍看视频");
}
}
}
(3)Thread 这里使用的事静态代理模式
- 真实角色 我们这个Programmer类
- 代理角色 Thread 类
两者都实现了run方法,代理角色要持有真实角色的引用 new Thread(programmer)
public class Start {
public static void main(String[] args) {
Programmer programmer = new Programmer();
Thread proxy = new Thread(programmer);
proxy.start();
for (int i = 0; i < 1000; i++) {
System.out.println("一遍聊QQ");
}
}
}
3、通过Callable接口实现多线程
(1)步骤
- 创建Callable实现类+重写call
- 借助调度服务ExecutorService,获得Future对象
ExecutorService ser= Executors.newFixedThreadPool(2);
Future result = ser.submit(实现类对象)
- 获取值 result.get()
- 停止服务ser.shutdownNow()
(2)优势
- 与实行Runnable相比, Callable功能更强大些
- 可以有返回值, 支持泛型的返回值
- 可以抛出异常
(3)线程池
Java通过Executors提供四种线程池,分别为:
- newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 - newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 - newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。 - newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
(4)具体实现
Future接口
- 可以对具体Runnable、 Callable任务的执行结果进行取消、 查询是否完成、 获取结果等。
public class Call {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个可重用固定线程数的线程池
ExecutorService ser = Executors.newFixedThreadPool(2);
Race tortoise = new Race("千年王八",1000L);
Race rabbit = new Race("兔子",500L);
//FutureTask 也可以了解一下
Future<Integer> result = ser.submit(tortoise);
Future<Integer> result2 = ser.submit(rabbit);
Thread.sleep(2000);
tortoise.setFlag(false);
rabbit.setFlag(false);
int num1 = result.get();
int num2 = result2.get();
System.out.println("乌龟跑了————>"+num1);
System.out.println("兔子跑了————>"+num2);
ser.shutdownNow();
}
}
class Race implements Callable<Integer> {
private String name;//名称
private long time;//延时时间
private boolean flag = true;
private int step = 0;
public Race() {
}
public Race(String name) {
this.name = name;
}
public Race(String name, long time) {
this.name = name;
this.time = time;
}
@Override
public Integer call() throws Exception {
while (flag){
Thread.sleep(time);
step++;
}
return step;
}
Getter Setter方法省略……
}
FutureTask实现类
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。 它既可以作为Runnable被线程执行, 又可以作为Future得到Callable的返回值
//第一种方式
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);
executor.shutdown();
//第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
Thread thread = new Thread(futureTask);
thread.start();
三、线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
1、新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
例如:Thread t1=new Thread();
2、就绪(runnable)
线程对象创建后,其他线程调用了该对象的start()方法,线程已经被启动,等待获取CPU的使用权。例如:t1.start();
3、运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
4、堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡(dead)
线程会如以下三种方式结束,结束后就处于死亡状态
- run() 或 call() 方法执行完成,线程正常结束
- 线程抛出一个未捕获的 Exception 或 Error
- 直接调用该线程的 stop() 方法来结束该线程——该方法容易导致死锁,不推荐使用
用一个开关控制线程的结束
public class StopDemo {
public static void main(String[] args) {
Study s = new Study();
new Thread(s).start();
for (int i = 0; i < 500; i++) {
if (250 == i) {
s.stop();
}
System.out.println("main线i="+i);
}
}
}
class Study implements Runnable{
private boolean flag = true;
@Override
public void run() {
while (flag){
System.out.println("study thread ");
}
}
public void stop(){
this.flag = false;
}
}
四、控制线程
1、调整线程优先级
Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY //线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY //线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY //分配给线程的默认优先级,取值为5。
Thread 类的 setPriority() 和 getPriority() 方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
public class PriorityDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread1 = new MyThread();
Thread t1 = new Thread(myThread1, "myThread1");
MyThread myThread2 = new MyThread();
Thread t2 = new Thread(myThread2, "myThread2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
Thread.sleep(100);
myThread1.stop();
myThread2.stop();
}
}
2、线程睡眠
Thread.sleep(long millis) 方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
public class SleepDemo extends Thread {
public static void main(String[] args) throws InterruptedException {
SleepDemo sleepDemo = new SleepDemo();
sleepDemo.start();
for (int i = 0; i < 10; i++) {
if (i == 5) {
System.out.println("sleep 5s");
sleep(10000);
}
System.out.println("main"+i);
}
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("sleep"+i);
}
}
}
3、线程等待
Object类中的 wait() 方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
都只能在同步
4、线程让步
Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
public class YieldDemo01 extends Thread {
public static void main(String[] args) throws InterruptedException {
YieldDemo01 demo01 = new YieldDemo01();
demo01.start();
for (int i = 0; i < 100; i++) {
if (i % 30 == 0) {
//暂停本线程 但不一定会成功
Thread.yield();
}
System.out.println("main" + i);
}
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("Join" + i);
}
}
}
5、线程加入
join() 方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
public class JoinDemo01 extends Thread {
public static void main(String[] args) throws InterruptedException {
JoinDemo01 joinDemo01 = new JoinDemo01();
joinDemo01.start();
for (int i = 0; i < 10; i++) {
if (i == 5) {
joinDemo01.join();
}
System.out.println("main"+i);
}
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Join"+i);
}
}
}
6、线程唤醒
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
7、后台线程(Daemon)
它是在后台运行,他的任务是为其他线程提供服务,也被称为“守护线程”或“精灵线程”。JVM垃圾回收就是典型的后台线程。
特征: 如果所有前台线程都死亡,后台线程也会死亡;主线程中创建的线程默认是前台线程,后台线程默认的是后台线程
setDaemon(true): 将线程设置成后台线程
isDaemon(): 用来判断线程是否是后台线程
五、线程同步机制
为什么要进行线程同步?
当多个线程同时操作同一个可共享资源变量时,如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突
1、同步块
synchronized(Object){
}
2、同步方法
synchronized void f(){
}
如果同步函数被静态修饰之后,使用的锁是什么?静态方法中不能定义this!
静态内存是:内存中没有本类对象,但是一定有该类对应的字节码文件对象。 类名.class 该对象类型是Class。
所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。 类名.class。代码如下:
public static mySyn(String name){
synchronized (Xxx.class) {
Xxx.name = name;
}
}
同步的前提:
1、必须要有两个或者两个以上的线程。
2、必须是多个线程使用同一个锁。
3、必须保证同步中只能有一个线程在运行。
4、只能同步方法,不能同步变量和类。
5、不必同步类中所有方法,类可以拥有同步和非同步的方法。
6、如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
7、线程睡眠时,它所持的任何锁都不会释放。
好处:解决了多线程的安全问题。
弊端:多个线程需要判断,消耗资源,降低效率。
如何找问题?
1、明确哪些代码是多线程运行代码。
2、明确共享数据。
3、明确多线程运行代码中哪些语句是操作共享数据的。
六、死锁
进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。
死锁形成的必要条件总结(都满足之后就会产生):
1、互斥条件:资源不能被共享,只能被同一个进程使用;
2、请求与保持条件:已经得到资源的进程可以申请新的资源;
3、非剥夺条件:已经分配的资源不能从相应的进程中强制剥夺;
4、循环等待条件:系统中若干进程形成环路,该环路中每个进程都在等待相邻进程占用的资源。
/**
* 一个场景,共同的资源
* 生产者消费者模式 信号灯法
*/
public class Movie {
private String pic;
/**
* 信号灯
* flag true 生产者生产,消费者等待,生产完成后通知消费者
* flag false 消费者消费,生产者等待,消费完成后通知生产者
*/
private boolean flag = true;
/**
* 播放
*/
public synchronized void play(String pic) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始生产
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//生产完毕
this.pic = pic;
System.out.println("生产了" + pic);
//通知消费
this.notify();
//生产者停下
this.flag = false;
}
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始消费
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费了" + pic);
//消费完毕
//通知生产
this.notifyAll();
//消费停止
this.flag = true;
}
}