Java 多线程
文章较长,给大家提供索引:
7.等待(wait())、唤醒(notify()、notifyAll())、休眠(sleep())
8.消费者-生产者问题(consumer-producter)
1.多线程的概念
首先明确两个概念:进程与线程。
进程:一个进程对应了一个应用程序。进程是某个数据集合的一次执行过程,也是操作系统进行资源分配和保护的基本单位。比如我们打开QQ,QQ在系统中就是一个进程,我们打开任务管理器,每一个大项就是一个进程。
线程:线程是进程的具体执行场景,一个进程可以包含多个线程。最简单的例子,我们用Chrome浏览器打开多个网页,在任务管理器里面可以看见一个Chrome进程包含了多个线程,每个线程就是我们具体的使用场景(网页)。
进程与进程间的内存是独立的,也就是说,每个进程都有自己的一块专属空间。但是线程间会共享堆内存与方法区(栈内存每个进程都有一个)。
并行和并发:
并行:多个CPU同时干一个事儿,或者是多台电脑,是真正的同时。
并发:CPU在多个任务间进行快速切换,切换规则根据CPU的调度算法指定。因为CPU执行速度太快,我们看上去像是在同时运行。
2.我们为什么要应用多线程?
多线程可以提高应用程序的利用率。事实上,所有的多线程都可以通过单线程写出来。
3.多线程的定义方式
第一种:通过继承Thread类。
1 class Demo extends Thread 2 { 3 public void run() 4 { 5 for (int i = 0; i < 10; i++) 6 System.out.print("a+"+i+" "); 7 } 8 } 9 10 class ThreadDemo 11 { 12 public static void main(String args[]) 13 { 14 15 Demo d = new Demo(); 16 d.start(); 17 for(int i=0;i<10;i++) 18 { 19 System.out.print("b+"+i+" "); 20 } 21 } 22 }
运行结果:
我们发现两个输出是交替运行的,说明多线程具有随机性。
具体步骤:
1.定义一个类,继承Thread;
2.覆盖Thread类中的run()方法;
run()方法里面写你想多线程执行的代码。
3.调用线程的start()方法。
start()两个作用:启动线程,调用run()方法。
向下面这样的是不行的:
1 class Demo extends Thread 2 { 3 public void run() 4 { 5 for (int i = 0; i < 10; i++) 6 System.out.print("a+"+i+" "); 7 } 8 } 9 10 class ThreadDemo 11 { 12 public static void main(String args[]) 13 { 14 15 Demo d = new Demo(); 16 d.run(); 17 for(int i=0;i<10;i++) 18 { 19 System.out.print("b+"+i+" "); 20 } 21 } 22 }
对比输出我们发现,如果只调用run()就跟一般的对象建立与方法调用无二了。
所以要调用start()而不是去自己调用run()。
第二种:实现Runnable接口
1 class Demo implements Runnable 2 { 3 public void run() 4 { 5 System.out.println("Thread Running!"); 6 } 7 } 8 9 class ThreadDemo 10 { 11 public static void main(String args[]) 12 { 13 14 Demo d=new Demo(); 15 Thread t=new Thread(d); 16 t.run(); 17 } 18 }
步骤:
1.定义类实现Runnable接口;
2.覆盖Runnable接口中的run()方法,将线程要运行的代码存放在该run方法中;
3.通过Thread类建立线程对象。
4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象作为实际参数传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法的话,就必须明确该run方法所属对象。
5.调用Thread类的start方法开启线程并调用Runnable接口子类的run()方法
一般来说,我们是推荐Runnable方式来定义的。因为可以避免单继承的局限性。
4.线程的运行状态
不多说,直接上图。
一个线程被创建(new())之后,会进入准备(start())状态。然后CPU通过调度使线程进入运行(run())状态,也就是说,线程被执行有且只有一个机会,就是进入start()状态,才有机会执行。
5.线程的安全问题,同步
我们知道,CPU的运行、切换速度是极快的。这样就会产生一个问题:两个线程操作同一个数据(共享数据)时,A线程向里面存放数据,B从里面取出数据,A的数据还没放完B就取走了。这样就导致取出的数据重复。又或者,A存数据时B还没来得及取,A就继续向里面存了,这样导致少取了几个数据。这两种情况都会导致数据错乱,所以我们必须去解决这个问题。Java为我们提供了同步(synchronized)来解决问题。
不加同步,我们进行下面的操作:
1 class Res 2 { 3 private String name; 4 private String sex; 5 6 public String getName() 7 { 8 return name; 9 } 10 11 public String getSex() 12 { 13 return sex; 14 } 15 16 public void setName(String name) 17 { 18 this.name = name; 19 } 20 21 public void setSex(String sex) 22 { 23 this.sex = sex; 24 } 25 } 26 27 class In implements Runnable 28 { 29 private Res res; 30 private boolean flag; 31 32 public In(Res res) 33 { 34 this.res = res; 35 } 36 37 @Override 38 public void run() 39 { 40 while (true) 41 { 42 if (flag) 43 { 44 res.setName("小红!!!"); 45 res.setSex("女!!!!"); 46 } 47 else 48 { 49 res.setName("小明"); 50 res.setSex("男"); 51 } 52 flag = !flag; 53 } 54 55 56 } 57 } 58 59 class Out implements Runnable 60 { 61 private Res res; 62 63 public Out(Res res) 64 { 65 this.res = res; 66 } 67 68 @Override 69 public void run() 70 { 71 while (true) 72 { 73 System.out.println("姓名是:" + res.getName()); 74 System.out.println("性别是:" + res.getSex()); 75 } 76 77 } 78 } 79 80 class synchronizedDemo 81 { 82 public static void main(String[] args) 83 { 84 Res res = new Res(); 85 Thread thread1 = new Thread(new In(res)); 86 Thread thread2 = new Thread(new Out(res)); 87 thread1.start(); 88 thread2.start(); 89 } 90 }
代码很简单,就是一个存数据一个取数据。
当我们运行之后:
也很好理解,原因就像上面说过的那样,数据错乱了。
同步:在一个线程运行的时候,只能等该线程运行完毕之后才能让其他线程参与操作,这就保证了线程对数据操作的唯一性。
方式1:同步代码块:
我们在循环外加上:
1 public void run() 2 { 3 while (true) 4 { 5 synchronized (res)//修改代码 6 { 7 if (flag) 8 { 9 res.setName("小红!!!"); 10 res.setSex("女!!!!"); 11 } 12 else 13 { 14 res.setName("小明"); 15 res.setSex("男"); 16 } 17 flag = !flag; 18 } 19 } 20 } 21 //只保留了修改的代码 22 @Override 23 public void run()//修改代码 24 { 25 while (true) 26 { 27 synchronized (res) 28 { 29 System.out.println("姓名是:" + res.getName()); 30 System.out.println("性别是:" + res.getSex()); 31 } 32 } 33 }
这样我们的打印结果就是正确的了。
原理:
synchronized相当于一个锁,每一个线程进去之后会持有该锁。只有这个线程将同步代码块内的代码执行完毕之后,这个锁才会被释放。在执行过程中,如有其他线程也要去执行被同步的代码,因为该锁已经被占有,所以就不会去执行里面的代码而是进入了锁定(block())状态。只有锁被释放,这些被锁定的线程才回去争抢执行资格。可以很形象的类比高铁上的厕所,因为只有一个位置,所以一次只能进一个人。这个人进去之后为防流氓会把门锁上(持有锁),只有等他解决完才能开门(释放锁)。然后门口等待的人就可以争抢这个位置了。
但是我们要注意三点:
1.一定是多线程环境。
2.多线程涉及到了共享数据且进行了修改。
3.同步的锁必须唯一。
也就是说,我们这里面传入的是res而不是this,就因为this不唯一。res我们只建立了一个对象当然是唯一的。其实,我们传入In.class,Out.class等等都可以,唯一就行。
方式2:同步函数
上面的方式是在需要被同步的函数外面套了一层同步代码块,我们还有另一种方式来实现,就是同步函数。
实现也很简单,就是在函数的返回值前面加上synchronized关键字就可以了,这个函数就是同步函数了。效果与同步代码块无二。
1 public synchronized void add(){}
但是如果是静态函数呢?
我们知道,静态函数不依赖对象的存在而存在,所以用对象作为静态函数的同步代码块的锁是错误的且没有意义的。这时候,我们就要使用类(.class)作为锁,因为在编译的时候字节码文件是唯一的。
6.死锁
死锁也很好理解:你持有我的锁,我持有你的锁,两个锁都在等待对方锁的释放。死锁的结果就是程序停滞,无法继续。
死锁的出现,一般是不正确的同步嵌套。
1 class DeadLock implements Runnable 2 { 3 @Override 4 public void run() 5 { 6 synchronized (Lock.object1) 7 { 8 System.out.println("123"); 9 synchronized (Lock.object2) 10 { 11 System.out.println("456"); 12 } 13 } 14 } 15 } 16 class Lock 17 { 18 static Object object1 = new Object(); 19 static Object object2 = new Object(); 20 21 } 22 class sychronizedDemo 23 { 24 public static void main(String[] args) 25 { 26 Thread thread = new Thread(new DeadLock()); 27 thread.start(); 28 } 29 }
7.等待(wait())、唤醒(notify()、notifyAll())、休眠(sleep())
在线程的控制中有四个方法比较关键:
wait():使一个线程进入等待阻塞状态(blocked),此时这个线程放弃了CPU的执行权,只有被唤醒才能重新参与争夺。这个方法必须在synchronzed内。
notify():唤醒处于等待阻塞的线程。
notifyAll():唤醒所有等待的线程。
(以上三个方法都会抛出异常。具体的异常这里不提,只需要知道如果使用了这些方法,或者try或者throws)
需要注意的是,这三个方法都是属于Objec类中的方法,也就说所有对象都具有这几个方法。这么做的用意是?
因为这些方法在操作同步中的线程是,都必须标示出他们所操作的线程持有的锁。只有同一个锁上的处于等待中的线程,才可以被同一个锁上的notify()唤醒。不可以对不同锁中的不同线程进行唤醒。但是,如果所有线程都在这个锁上等待,这时notify会随机唤醒其中一个线程。
sleep():使线程进入休眠状态。需要注意的是,在休眠状态的线程并没有交出执行权。我们可以指定休眠时间,比如sleep(10)。
了解了这四个方法,我们就可以解决下面的经典问题了——
8.消费者-生产者问题(consumer-producter)
首先先简单的构建一个场景--
在一个采矿场,有两拨工人——一波负责挖煤,一波负责将煤运出去。挖煤的人会将挖好的煤放入一个大桶,然后运煤的会从桶里面将煤运走。假设两拨工人的效率是一样的,也就是挖和踩的速度是一样的。我们希望,桶内不要存煤,换句话说,挖了一块放在桶里,立刻就有人将桶内的煤运走。
这个问题,就涉及到了我们的生产者-消费者模型。我们会用三个方法来解决这个问题。
问题分析:
我们知道,不同线程会去争夺CPU的执行权。也就是说,如果第一次挖煤的抢到了执行权,然后下一次是挖煤还是运煤是不确定的。我们希望的是,当我们挖煤的时候,运煤的不去干扰我们,等我们把煤挖完之后,运煤的安心运煤同样不让挖煤的干扰,这样两个动作交替运行,就会有和谐的结果。
这里面引入两个概念:
等待池:假设一个线程执行了wait()方法,那么这个线程就会释放掉自己的锁(因为这个线程既然执行了wait方法,那么它一定实在synchornized内,也就是说这个线程一定持有锁),然后进入等待池中。等待池中的线程不会去竞争执行权。
同步锁池:在一个线程获得对象的锁的时候,其他线程就在同步锁池中等待。在这个线程释放掉自己的锁之后,就进入了等待池中。notify()方法会(随机)唤醒等待池中的一个方法进入到同步锁池中进行竞争,而notityAll()会唤醒所有的线程。
关于这俩概念,我这里有个比喻可能会帮助理解。
在古代,想进入官场(执行方法体)出人头地就必须去科举考试,谁第一谁当官(获取到锁,也就是持有锁)。而众所周知,有这个想法的人很多,大家都在考试(竞争CPU执行权),大家都在考场进行考试(同步锁池)。但是官场阴暗,现任官员被罢官(执行了对象的wait()方法),这个官员就去了深山老林(等待池)。这时候,有人对他进行了鼓舞(notify()),他很兴奋,但是想当官也是需要重新考的,于是和大家一起考试(进入了同步锁池)。当然,官位不能空缺,在他隐居深山的时候,自然有人通过竞争当了官。
介绍完概念,我们回到问题上。首先把问题简化,假设只有挖煤人A,运煤人B,一个挖一个运,效率相同。
解决方式1:
我们希望是这样的:
①A执行T对象的同步方法,此时对象持有T对象的锁,B在T的锁池中等待。
②A执行到了wait()方法,A释放锁,进入了T的等待池,此时A进入了同步锁池,与其他的线程一通参与竞争。
③在锁池中的B拿到锁,执行它自己的同步方法。
④B执行到了notify(),唤醒了在等待池中的A并将其移动到了T对象的锁池中等待获取锁。
⑤B执行完了同步方法,释放锁,A获取锁,继续①。
1 class Res //共享资源 2 { 3 int age = 0; 4 String name; 5 boolean isEmpty = true;//资源是否为空 6 7 public synchronized void In(String name, int age)//生产方法 8 { 9 try 10 { 11 while (!isEmpty)//如果资源非空 12 { 13 this.wait();//等待 14 } 15 this.name = name; 16 this.age = age; 17 isEmpty = false;//生产完毕,资源非空 18 this.notifyAll(); 19 } catch (Exception e) 20 { 21 e.printStackTrace(); 22 } 23 } 24 25 public synchronized void Out()//消费方法 26 { 27 try 28 { 29 while (isEmpty)//资源为空 30 { 31 this.wait();//等待 32 } 33 System.out.println("姓名:" + name + "年龄:" + age); 34 isEmpty = true; 35 this.notifyAll(); 36 } catch (Exception e) 37 { 38 e.printStackTrace(); 39 } 40 } 41 } 42 43 44 class Producer implements Runnable 45 { 46 private Res res; 47 private int i = 0; 48 49 public Producer(Res res) 50 { 51 this.res = res; 52 } 53 54 @Override 55 public void run() 56 { 57 while (true) 58 { 59 if (i % 2 == 0) 60 res.In("小红", 10); 61 else 62 res.In("老王", 70); 63 i++; 64 } 65 66 } 67 } 68 69 class Consumer implements Runnable 70 { 71 private Res res; 72 73 public Consumer(Res res) 74 { 75 this.res = res; 76 77 } 78 79 @Override 80 public void run() 81 { 82 while (true) 83 { 84 res.Out(); 85 } 86 87 88 } 89 } 90 91 class synchronizedDemo 92 { 93 public static void main(String[] args) 94 { 95 Res res = new Res();//分别创建了两个生产者两个消费者,更能突出现象 96 Thread thread1 = new Thread(new Consumer(res)); 97 Thread thread2 = new Thread(new Producer(res)); 98 Thread thread3 = new Thread(new Consumer(res)); 99 Thread thread4 = new Thread(new Producer(res)); 100 thread1.start(); 101 thread2.start(); 102 thread3.start(); 103 thread4.start(); 104 105 } 106 }
上面的代码就解决了问题——
稍加说明:
同步函数的锁是this,也就是当前对象。因为在main函数中我们只创建了一个Res对象,所以自始至终两个函数(In,Out)用的是同一个锁。
解决方式2:
在JDK升级到5.0之后,java为我们提供了一个新的解决方式:Lock包。
以往我们是使用sychronized关键字来隐式的建立锁、释放锁的(我们从没手动干涉过锁的建立,也没手动释放锁(wait是个例外,其实它也不算是正常释放锁,因为wait之后线程进入了等待池,而正常情况下应该进入锁池))。Java为我们提供了手动创建锁和释放锁的方式,并将我们上述的三个方法(wait,notify,notifyAll)与锁挂上了钩。下面详细说明。
我们将上面的“生产者消费者1”进行Lock的修改:
1 import java.util.concurrent.locks.Condition; 2 import java.util.concurrent.locks.Lock; 3 import java.util.concurrent.locks.ReentrantLock; 4 5 class Res //共享资源 6 { 7 int age = 0; 8 String name; 9 boolean isEmpty = true;//资源是否为空 10 Lock lock = new ReentrantLock(); 11 private Condition conditionOfConusmer=lock.newCondition(); 12 private Condition conditionOfProducer=lock.newCondition(); 13 public void In(String name, int age)//生产方法 14 { 15 lock.lock(); 16 try 17 { 18 while (!isEmpty)//如果资源非空 19 { 20 conditionOfProducer.await();//等待 21 } 22 this.name = name; 23 this.age = age; 24 isEmpty = false;//生产完毕,资源非空 25 conditionOfConusmer.signal(); 26 } catch (Exception e) 27 { 28 e.printStackTrace(); 29 }finally 30 { 31 lock.unlock(); 32 } 33 } 34 35 public synchronized void Out()//消费方法 36 { 37 lock.lock(); 38 try 39 { 40 while (isEmpty)//资源为空 41 { 42 conditionOfConusmer.await();//等待 43 } 44 System.out.println("姓名:" + name + " 年龄:" + age); 45 isEmpty = true; 46 conditionOfProducer.signal(); 47 } catch (Exception e) 48 { 49 e.printStackTrace(); 50 }finally 51 { 52 lock.unlock(); 53 } 54 } 55 } 56 57 58 class Producer implements Runnable 59 { 60 private Res res; 61 private int i = 0; 62 63 public Producer(Res res) 64 { 65 this.res = res; 66 } 67 68 @Override 69 public void run() 70 { 71 while (true) 72 { 73 if (i % 2 == 0) 74 res.In("小红", 10); 75 else 76 res.In("老王", 70); 77 i++; 78 } 79 80 } 81 } 82 83 class Consumer implements Runnable 84 { 85 private Res res; 86 87 public Consumer(Res res) 88 { 89 this.res = res; 90 91 } 92 93 @Override 94 public void run() 95 { 96 while (true) 97 { 98 res.Out(); 99 } 100 101 102 } 103 } 104 105 class synchronizedDemo 106 { 107 public static void main(String[] args) 108 { 109 Res res = new Res();//分别创建了两个生产者两个消费者,更能突出现象 110 Thread thread1 = new Thread(new Consumer(res)); 111 Thread thread2 = new Thread(new Producer(res)); 112 Thread thread3 = new Thread(new Consumer(res)); 113 Thread thread4 = new Thread(new Producer(res)); 114 thread1.start(); 115 thread2.start(); 116 thread3.start(); 117 thread4.start(); 118 119 } 120 }
我们将生产消费方法的sychronized关键字去掉,使用了Lock类提供的方法。
Lock是一个接口,我们使用的是他的以实现子类ReentrantLock来实例化对象。
1 Lock lock = new ReentrantLock();
这样我们就拿到了一个可以自己操作的锁对象。然后在函数首使用lock.lock()方法来获取锁。在finally中去释放锁lock.unlock()。(因为如果发生异常,程序会停掉,但是此时的线程仍然持有锁。所以我们无论是否发生异常,释放锁是一定会做的,也就是放在finally中)。
Condition也是一个接口。Condition将以前对于的Object的方法(wait,notify,notifyAll)分成了截然不同的对象,这些对象就可以去与不同的锁搭配使用。而且一个锁可以对应多个Condition,而Object的方法只能对应一个锁。3
换言之,一个锁就对应这一个阻塞队列,Condition可以操作多个队列。
使用方法:
1 Condition conditionr=lock.newCondition();
其中lock是你指定的锁。也就是说,之前的wait等方法,只是对于一个锁而言的,我们不能去操作别的锁(如果跨锁操作,就意味着有两个以上的锁,这样就会涉及死锁倾向)。现在我们用Condition替换,就能去操作指定的锁上的线程:
condition.await(); 对应wait();
condition.singal(); 对应notify();
condition.signalAll(); 对应notifyAll();
拿刚才的程序来讲;
1 conditionOfConusmer.signal();
这句话就很清晰的在生产者中去唤醒了消费者的线程。以前是用notifyAll(),不管本方他方全部唤醒,然后去循环判断标记。与指定方向的唤醒比较,显然后者更清晰效率更高。
9.守护线程
守护线程见名知意,这个线程会自创建以来一直运行,直到主线程结束,主线程结束,所有守护线程全部结束。
1 Thread thread1 = new Thread(); 2 thread1.setDaemon(true);
这样就可以将一个线程设置为守护线程。
10.join方法
当A线程执行到了B线程的join方法时,A就会等待,直到B线程执行完毕,A才会执行。
join可以用来临时加入线程执行。
11.线程组
线程组很好理解,谁开启的线程,这个线程就属于那个线程组。
比如在main函数里面开启thread1,thread2,那么这俩就属于main组。
我们可以用thread.toString来详细线程的名称、优先级、线程组等详细信息。
12.优先级
优先级,一个线程的优先级越高,它被CPU执行的机会就越大。
我们用setPriority(int newPriority)来设置优先级(0-10)。默认的优先级是5。
但是用数字来表示优先级不是很明显,5与6的优先级我们可能并不能感觉到明显差别。所以,我们定义了三个常量,来表示三个级别的优先级:MIN_PRIORITY、MAX_PRIOROTY、NORM_PRIORITY。
1 thread1.setPriority(MAX_PRIORITY);
至此,多线程部分就总结完毕。对于初学者,这些内容已经足够了。才疏学浅,有错误欢迎指正。倾心总结,希望捧场。