------- android培训、java培训、期待与您交流! ----------
多线程
一、多线程的概念
(1)进程、线程、多进程的概念
进程:正在进行中的程序(直译)。
线程:进程中一个负责程序执行的控制单元(执行路径)。
注释:
1、一个进程中可以有多个执行路径,称之为多线程。
2、一个进程中至少要有一个线程。
3、开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
多线程的好处:解决了多部分代码同时运行的问题。
多线程的弊端:线程太多,会导致效率的降低。
其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。
JVM启动时启动了多条线程,至少有两个线程可以分析的出来:
1. 执行main函数的线程,该线程的任务代码都定义在main函数中。
2. 负责垃圾回收的线程。
(2)创建线程方式一:继承Thread类
1. 定义一个类继承Thread类。
2. 覆盖Thread类中的run方法。
3. 直接创建Thread的子类对象创建线程。
4. 调用start方法开启线程并调用线程的任务run方法执行。
创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。而自定义的线程,它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。
开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。
(3)创建线程方式二:实现Runnable接口
1. 定义类实现Runnable接口。
2. 覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3. 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务。
4. 调用线程对象的start方法开启线程。
实现Runnable接口的好处:
1. 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
2. 避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。
示例:
1. //准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
2. //通过接口的形式完成。
3. class Demo implements Runnable{
4. public void run(){
5. show();
6. }
7. public void show(){
8. for(int x = 0; x < 20; x++){
9. System.out.println(Thread.currentThread().getName() + "..." + x);
10. }
11. }
12. }
13.
14. class ThreadDemo{
15. public static void main(String[] args){
16. Demo d = new Demo();
17. Thread t1 = new Thread(d);
18. Thread t2 = new Thread(d);
19. t1.start();
20. t2.start();
21. }
22. }
Thread类、Runnable接口内部源码关系模拟代码:
1. class Thread{
2. private Runnable r ;
3. Thread(){
4. }
5. Thread(Runnable r){
6. this.r = r;
7. }
8.
9. public void run(){
10. if(r !=null)
11. r.run();
12. }
13. public void start(){
14. run();
15. }
16. }
17.
18. class ThreadImpl implements Runnable{
19. public void run(){
20. System.out.println("runnable run" );
21. }
22. }
23.
24. class ThreadDemo4{
25. public static void main(String[] args){
26. ThreadImpl i = new ThreadImpl();
27. Thread t = new Thread(i);
28. t.start();
29. }
30. }
31.
32. class SubThread extends Thread{
33. public void run(){
34. System.out.println("hahah" );
35. }
36. }
37.
38. class ThreadDemo5{
39. public static void main(String[] args){
40. SubThread s = new SubThread();
41. s.start();
42. }
43. }
二、线程安全问题
(1)线程安全问题产生的原因
需求:模拟4个线程同时卖100张票。
代码:
1. class Ticket implements Runnable{
2. private int num = 100;
3.
4. public void run(){
5. while(true ){
6. if(num > 0){
7. try{
8. Thread. sleep(10);
9. } catch(InterruptedException e){
10. e.printStackTrace();
11. }
12. System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
13. }
14. }
15. }
16. }
17.
18. class TicketDemo{
19. public static void main(String[] args){
20. Ticket t = new Ticket();
21. Thread t1 = new Thread(t);
22. Thread t2 = new Thread(t);
23. Thread t3 = new Thread(t);
24. Thread t4 = new Thread(t);
25.
26. t1.start();
27. t2.start();
28. t3.start();
29. t4.start();
30. }
31. }
原因分析:
出现上图安全问题的原因在于Thread-0通过了if判断后,在执行到“num--”语句之前,num此时仍等于1。
CPU切换到Thread-1、Thread-2、Thread-3之后,这些线程依然可以通过if判断,从而执行“num--”的操作,因而出现了0、-1、-2的情况。
线程安全问题产生的原因:
1. 多个线程在操作共享的数据。
2. 操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
(2)线程安全问题的解决方案
思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。
必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在java中,用同步代码块就可以解决这个问题。
同步代码块的格式:
synchronized(对象){
需要被同步的代码;
}
同步的好处:解决了线程的安全问题。
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
同步的前提:必须有多个线程并使用同一个锁。
注释:
同步函数和同步代码块的区别:
1. 同步函数的锁是固定的this。
2. 同步代码块的锁是任意的对象。
建议使用同步代码块。
由于同步函数的锁是固定的this,同步代码块的锁是任意的对象,那么如果同步函数和同步代码块都使用this作为锁,就可以实现同步。
(3)多线程下的单例模式
饿汉式:
1. class Single{
2. private static final Single s = new Single();
3. private Single(){}
4. public static Single getInstance(){
5. return s ;
6. }
7. }
注释:
饿汉式不存在安全问题,因为不存在多个线程共同操作数据的情况。
懒汉式:
1. class Single{
2. private static Single s = null;
3. private Single(){}
4. public static Single getInstance(){
5. if(s ==null){
6. synchronized(Single.class){
7. if(s == null)
8. s = new Single();
9. }
10. }
11. return s ;
12. }
13. }
注释:
懒汉式存在安全问题,可以使用同步函数解决。
但若直接使用同步函数,则效率较低,因为每次都需要判断。
原因在于任何一个线程在执行到第一个if判断语句时,如果Single对象已经创建,则直接获取即可,而不用判断是否能够获取锁,相对于上面使用同步函数的方法就提升了效率。如果当前线程发现Single对象尚未创建,则再判断是否能够获取锁。
1. 如果能够获取锁,那么就通过第二个if判断语句判断是否需要创建Single对象。因为可能当此线程获取到锁之前,已经有一个线程创建完Single对象,并且放弃了锁。此时它便没有必要再去创建,可以直接跳出同步代码块,放弃锁,获取Single对象即可。如果有必要,则再创建。
2. 如果不能获取到锁,则等待,直至能够获取到锁为止,再按步骤一执行。
(4)死锁示例
死锁常见情景之一:同步的嵌套。
示例1:
1. class Ticket implements Runnable{
2. private static int num = 100;
3. Object obj = new Object();
4. boolean flag = true;
5.
6. public void run(){
7. if(flag ){
8. while(true ){
9. synchronized(obj ){
10. show();
11. }
12. }
13. } else
14. while(true )
15. show();
16. }
17.
18. public synchronized void show(){
19. synchronized(obj ){
20. if(num > 0){
21. try{
22. Thread. sleep(10);
23. } catch(InterruptedException e){
24. e.printStackTrace();
25. }
26. System.out.println(Thread.currentThread().getName() + "...function..." + num--);
27. }
28. }
29. }
30. }
31.
32. class DeadLockDemo{
33. public static void main(String[] args){
34. Ticket t = new Ticket();
35. Thread t1 = new Thread(t);
36. Thread t2 = new Thread(t);
37.
38. t1.start();
39. try{
40. Thread. sleep(10);
41. } catch(InterruptedException e){
42. e.printStackTrace();
43. }
44. t. flag = false ;
45. t2.start();
46. }
47. }
原因分析:
由上图可以看到程序已经被锁死,无法向下执行。
由下图代码可以看到,run方法中的同步代码块需要获取obj对象锁,才能执行代码块中的show方法。
而执行show方法则必须获取this对象锁,然后才能执行其中的同步代码块。
当线程t1获取到obj对象锁执行同步代码块,线程t2获取到this对象锁执行show方法。同步代码块中的show方法因无法获取到this对象锁无法执行,show方法中的同步代码块因无法获取到obj对象锁无法执行,就会产生死锁。
三、线程间通信
(1)线程间通信涉及的方法
多个线程在处理统一资源,但是任务却不同,这时候就需要线程间通信。
等待/唤醒机制涉及的方法:
1. wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
2. notify():唤醒线程池中的一个线程(任何一个都有可能)。
3. notifyAll():唤醒线程池中的所有线程。
注释:
1、这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法。
2、必须要明确到底操作的是哪个锁上的线程!
3、wait和sleep区别?
1)wait可以指定时间也可以不指定。sleep必须指定时间。
2)在同步中时,对CPU的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。
为什么操作线程的方法wait、notify、notifyAll定义在了object类中,因为这些方法是监视器的方法,监视器其实就是锁。
锁可以是任意的对象,任意的对象调用的方式一定在object类中。
生产者-消费者问题:
1. class Resource{
2. private String name ;
3. private String sex ;
4. private boolean flag = false;
5.
6. public synchronized void set(String name,String sex){
7. if(flag )
8. try{
9. this.wait();
10. } catch(InterruptedException e){
11. e.printStackTrace();
12. }
13. this.name = name;
14. this.sex = sex;
15. flag = true ;
16. this.notify();
17. }
18.
19. public synchronized void out(){
20. if(!flag )
21. try{
22. this.wait();
23. } catch(InterruptedException e){
24. e.printStackTrace();
25. }
26. System. out.println(name + "..." + sex);
27. flag = false ;
28. this.notify();
29. }
30. }
31.
32. //输入
33. class Input implements Runnable{
34. Resource r;
35. Input(Resource r){
36. this.r = r;
37. }
38.
39. public void run(){
40. int x = 0;
41. while(true ){
42. if(x == 0){
43. r.set( "mike","男" );
44. } else{
45. r.set( "lili","女" );
46. }
47. x = (x + 1)%2;
48. }
49. }
50. }
51.
52. //输出
53. class Output implements Runnable{
54. Resource r;
55.
56. Output(Resource r){
57. this.r = r;
58. }
59.
60. public void run(){
61. while(true ){
62. r.out();
63. }
64. }
65. }
66.
67. class ResourceDemo {
68. public static void main(String[] args){
69. //创建资源
70. Resource r = new Resource();
71. //创建任务
72. Input in = new Input(r);
73. Output out = new Output(r);
74. //创建线程,执行路径
75. Thread t1 = new Thread(in);
76. Thread t2 = new Thread(out);
77. //开启线程
78. t1.start();
79. t2.start();
80. }
81. }
多生产者-多消费者问题:
1. class Resource{
2. private String name ;
3. private int count = 1;
4. private boolean flag = false;
5.
6. public synchronized void set(String name){
7. if(flag )
8. try{
9. wait();
10. } catch(InterruptedException e){
11. e.printStackTrace();
12. }
13. this.name = name + count;
14. count++;
15. System.out.println(Thread.currentThread().getName() + "...生产者..." + this. name);
16. flag = true ;
17. notify();
18. }
19.
20. public synchronized void out(){
21. if(!flag )
22. try{
23. wait();
24. } catch(InterruptedException e){
25. e.printStackTrace();
26. }
27. flag = false ;
28. notify();
29. System.out.println(Thread.currentThread().getName() + "...消费者..." + this. name);
30. }
31. }
32.
33. class Producer implements Runnable{
34. private Resource r ;
35. Producer(Resource r){
36. this.r = r;
37. }
38. public void run(){
39. while(true ){
40. r.set( "烤鸭");
41. }
42. }
43. }
44.
45. class Consumer implements Runnable{
46. private Resource r ;
47. Consumer(Resource r){
48. this.r = r;
49. }
50. public void run(){
51. while(true ){
52. r.out();
53. }
54. }
55. }
56.
57. class ProducerConsumerDemo {
58. public static void main(String[] args){
59. Resource r = new Resource();
60. Producer pro = new Producer(r);
61. Consumer con = new Consumer(r);
62.
63. Thread t0 = new Thread(pro);
64. Thread t1 = new Thread(pro);
65. Thread t2 = new Thread(con);
66. Thread t3 = new Thread(con);
67. t0.start();
68. t1.start();
69. t2.start();
70. t3.start();
71. }
72. }
运行结果:以上代码存在安全问题。
原因分析:
得到以上结果的过程分析如下:
1. 线程Thread-0获取到CPU执行权及锁,生产了烤鸭3298,将flag设置为true。然后,Thread-0又重新获取到CPU执行权,由于flag为true,故执行wait方法,阻塞。Thread-1接着获取到CPU执行权,由于flag为true,故执行wait方法,也阻塞。
2. 线程Thread-3获取到CPU执行权及锁,消费了烤鸭3298,将flag设置为false。然后,线程Thread-0被唤醒,但是并没有获取到锁,而是线程Thread-3接着获取到CPU执行权及锁,然而此时flag为false,所以Thread-3阻塞。下面线程Thread-2接着获取到CPU执行权及锁,然而此时flag为false,所以Thread-2也阻塞。
3. 线程Thread-0获取到CPU执行权及锁,不需要if语句判断,直接生产烤鸭3299,然后又唤醒线程Thread-1获取到CPU执行权及锁,不需要if语句判断,直接生产烤鸭3300。从而造成了烤鸭3299还没有被消费,就直接生产了烤鸭3300的情况。
由于if判断标记,只有一次,会导致不该运行的线程运行了,出现了数据错误的情况。故修改成while判断标记,线程获取CPU执行权及锁后,将重新判断是否具备运行条件。
notify方法只能唤醒一个线程,如果本方唤醒了本方,没有意义。而且while判断标记+notify会导致死锁。notifyAll解决了本方线程一定会唤醒对方线程的问题。
注释:
while判断标记+notify会导致死锁的示例:
如果将上面的代码中的if判断标记修改成wile判断标记,就会出现死锁的现象,前2步与原来是一致的。第3步如下:
3. 线程Thread-0获取到CPU执行权及锁,通过了while语句判断,直接生产烤鸭3299,将flag设置为true。然后又唤醒线程Thread-1获取到CPU执行权及锁,没有通过while语句判断,阻塞。线程Thread-0又获取到CPU执行权及锁,通不过while语句判断,也阻塞,此时Thread-0、1、2、3都阻塞,故死锁。
(2)JDK1.5新特性
同步代码块就是对于锁的操作是隐式的。
JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock接口:出现替代了同步代码块或者同步函数,将同步的隐式操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器。
lock():获取锁。
unlock():释放锁,为了防止异常出现,导致锁无法被关闭,所以锁的关闭动作要放在finally中。
Condition接口:出现替代了Object中的wait、notify、notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
Condition接口中的await方法对应于Object中的wait方法。
Condition接口中的signal方法对应于Object中的notify方法。
Condition接口中的signalAll方法对应于Object中的notifyAll方法。
使用一个Lock、一个Condition修改上面的多生产者-多消费者问题。
(3)停止线程
怎么控制线程的任务结束呢?
任务中都会有循环结构,只要控制住循环就可以结束任务。
控制循环通常就用定义标记来完成。