黑马程序员——Java基础---多线程
一、多线程概述:
要了解多线程,就必须知道什么是线程。而要知道什么是线程就必须知道什么是进程。
1、进程:
进程是一个正在执行中的程序(几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序)。
每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
2、线程(Thread):
线程是进程中的一个独立的控制单元,线程在控制着进程的执行。一个进程至少有一个线程。
3、多线程:
在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。像种在一个进程中有多个线程执行的方式,就叫做多线程。
提示:归纳起来可以这样说:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
4、多线程的优势:
在实际应用中,多线程是非常有用的,一个浏览器必须能同时下载多个图片;一个Web服务器必须能同时响应多个用户请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收; 总之,多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。
二、创建线程的方式:
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例(继承Thread类创建线程类)或是实现Runnable接口创建线程类。
1、继承Thread类创建线程类:
通过继承Thread类来创建并启动多线程的步骤如下:
(1)、定义Thread的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。
(2)、创建Thread子类的实例,即创建了线程对象。
(3)、调用线程对象的start()方法来启动线程。注:如果对象直接调用run方法,等同于只有一个线程在执行,自定义的线程并没有启动。
下面程序示范了通过继承Thread类来创建并启动多线程。
// 通过继承Thread类来创建线程类
1 // 通过继承Thread类来创建线程类
2 public class FirstThread extends Thread
3 {
4 private int i ;
5 // 重写run方法,run方法的方法体就是线程执行体
6 public void run()
7 {
8 for ( ; i < 100 ; i++ )
9 {
10 // 当线程类继承Thread类时,直接使用this即可获取当前线程
11 // Thread对象的getName()返回当前该线程的名字
12 // 因此可以直接调用getName()方法返回当前线程的名
13 System.out.println(getName() + " " + i);
14 }
15 }
16 public static void main(String[] args)
17 {
18 for (int i = 0; i < 100; i++)
19 {
20 // 调用Thread的currentThread方法获取当前线程
21 System.out.println(Thread.currentThread().getName()
22 + " " + i);
23 if (i == 20)
24 {
25 // 创建、并启动第一条线程
26 new FirstThread().start();
27 // 创建、并启动第二条线程
28 new FirstThread().start();
29 }
30 }
31 }
32 }
2、实现Runnable接口创建线程类:
实现Runnable接口来创建并启动多线程的步骤如下:
(1)、定义类实现Runnable的接口。
(2)、覆盖Runnable接口中的run方法。目的也是为了将线程要运行的代码存放在该run方法中。
(3)、通过Thread类创建线程对象。
(4)、将Runnable接口的子类对象作为实参传递给Thread类的构造方法。
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。
(5)、调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。
实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。
程序示例:
1 package I10;
2 /*
3 需求:简单的卖票程序。
4 多个窗口卖票。
5 */
6
7 public class TicketTest1 implements Runnable
8 {
9 private int count=100;
10 public void run()
11 {
12 while(true)
13 {
14 if(count>0)
15 {
16 //显示线程名及余票数
17 System.out.println(Thread.currentThread().getName()+"----"+count--);
18 }
19 }
20 }
21 }
22 class Ticked1
23 {
24 public static void main(String[] args)
25 {
26
27 //创建Runnable接口子类的实例对象
28 TicketTest1 t1=new TicketTest1();
29 //有多个窗口在同时卖票,这里用三个线程表示
30 Thread thread1=new Thread(t1);
31 Thread thread2=new Thread(t1);
32 Thread thread3=new Thread(t1);
33 thread1.start();//启动线程
34 thread2.start();
35 thread3.start();
36 }
37 }
三、两种方式的区别和线程的几种状态
1、两种创建方式的区别
继承Thread:线程代码存放在Thread子类run方法中。
实现Runnable:线程代码存放在接口子类run方法中,实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。。
2、几种状态
被创建:等待启动,调用start启动---该线程处于就绪状态即等待执行。
运行状态:具有执行资格和执行权。
临时状态(阻塞):有执行资格,但是没有执行权。
冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。
消忙状态:stop()方法,或者run方法结束。
注:当已经从创建状态到了运行状态,再次调用start()方法时,就失去意义了,java运行时会提示线程状态异常。
四、线程安全问题
1、导致安全问题的出现的原因:
当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没用执行完,另一个线程参与进来执行。导致共享数据的错误。
简单的说就两点:
(1)、多个线程访问出现延迟。
(2)、线程随机性。
注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。
2、解决办法——同步
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)
这里也有两种解决方式,一种是同步代码块,还有就是同步函数。都是利用关键字synchronized来实现。
(1)、同步代码块
用法:
synchronized(对象)
{需要被同步的代码}
同步可以解决安全问题的根本原因就在那个对象上。其中对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
实例:
1 package I10;
2 /*
3 需求:简单的卖票程序。
4 多个窗口卖票。
5 */
6
7 public class TicketTest1 implements Runnable
8 {
9 private int count=100;
10 Object obj=new Object();
11 public void run()
12 {
13 while(true)
14 {
15 //给程序加同步,即锁
16 synchronized (obj)
17 {
18 if(count>0)
19 {
20 try {
21 //使用线程中的sleep方法,模拟线程出现的安全问题 因为sleep方法有异常声明,所以这里要对其进行处理
22 Thread.sleep(1000);
23 }
24 catch (InterruptedException e)
25 {
26 e.printStackTrace();
27 }
28 }
29 }
30 }
31 }
32 }
33 class Ticked1
34 {
35 public static void main(String[] args)
36 {
37
38 //创建Runnable接口子类的实例对象
39 TicketTest1 t1=new TicketTest1();
40 //有多个窗口在同时卖票,这里用三个线程表示
41 Thread thread1=new Thread(t1);
42 Thread thread2=new Thread(t1);
43 Thread thread3=new Thread(t1);
44 thread1.start();//启动线程
45 thread2.start();
46 thread3.start();
47 }
48 }
(2),同步函数
格式:
在函数上加上synchronized修饰符即可。
那么同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。
拿同步代码块的示例:
1 package I10;
2
3
4
5 public class TicketTest implements Runnable {
6 private int count=100;
7 public void run()
8 {
9 while(true)
10 {
11 show();
12 }
13 }
14 public synchronized void show()
15 {
16 if(count>0)
17 {
18 try {
19 Thread.sleep(100);
20 } catch (InterruptedException e) {
21 // TODO Auto-generated catch block
22 e.printStackTrace();
23 }
24 System.out.println(Thread.currentThread().getName()+"----"+count--);
25 }
26 }
27 }
28 class Ticked{
29 public static void main(String[] args) {
30 TicketTest t1=new TicketTest();
31 Thread thread1=new Thread(t1);
32 Thread thread2=new Thread(t1);
33 Thread thread3=new Thread(t1);
34 thread1.start();
35 thread2.start();
36 thread3.start();
37 }
38 }
3、同步的前提
a,必须要有两个或者两个以上的线程。
b,必须是多个线程使用同一个锁。
4、同步的利弊
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
5、如何寻找多线程中的安全问题
a,明确哪些代码是多线程运行代码。
b,明确共享数据。
c,明确多线程运行代码中哪些语句是操作共享数据的。
五、静态函数的同步方式
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:
类名.class 该对象的类型是Class
这就是静态函数所使用的锁。而静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class
单例模式:懒汉式
1 package I10;
2 /**
3 * 加同步的单例设计模式----懒汉式
4 *
5 */
6 public class Single
7 {
8 private static Single s=null;
9 private Single(){}
10 public static Single getInstance()
11 {
12 if(s==null)
13 {
14 synchronized(Single.class)
15 {
16 if(s==null)
17 s=new Single();
18 }
19 }
20 return s;
21 }
22 }
六、线程间通信
其实就是多个线程在操作同一个资源,但是操作的动作不同。
1、使用同步操作同一资源的示例:
package I10;
/*
有一个资源
一个线程往里存东西,如果里边没有的话
一个线程往里取东西,如果里面有得话
*/
//资源
class Res{
private String name;
private String sex;
boolean flag=false;//标记
//同步函数
public synchronized void set(String name,String sex){
//如果有资源等待资源取出
if(flag)
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.name=name;
this.sex=sex;
flag=true;//表示有资源
this.notify();//唤醒等待
}
public synchronized void out(){
//如果没有资源,等待存入资源
if(!flag)
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//这里用打印表示取出
System.out.println(name+"--"+sex);
flag=false;//资源已取出
this.notify();//等待唤醒
}
}
//存资源
class Input implements Runnable{
private Res r;
Input(Res r) {
this.r=r;
}
//重写run()方法
@Override
public void run() {
// TODO Auto-generated method stub
int x=0;
while(true)
{
if(x==0){
r.set("小四", "男");
}
else {
r.set("女侠", "女");
}
x=(x+1)%2;//控制交替打印
}
}
}
//取资源
public class Output implements Runnable{
private Res r;
Output(Res r){
this.r=r;
}
@Override
public void run() {
while(true){
r.out();
}
}
}
class OutInt2{
public static void main(String[] args) {
Res r=new Res();//表示操作的是同一个资源
new Thread(new Input(r)).start();//开启存线程
new Thread(new Output(r)).start();//开启取线程
}
}
几个小问题:
1)wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?
a,这些方法存在与同步中。
b,使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。
c,锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。
2)wait(),sleep()有什么区别?
wait():释放cpu执行权,释放锁。
sleep():释放cpu执行权,不释放锁。
3)为甚么要定义notifyAll?
因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。
2、JDK1.5中提供了多线程升级解决方案。
将同步synchronized替换成显示的Lock操作。将Object中wait,notify,notifyAll,替换成了Condition对象。该Condition对象可以通过Lock锁进行获取,并支持多个相关的Condition对象。
升级解决方案的示例:
1 package I10;
2
3 import java.util.concurrent.locks.Condition;
4 import java.util.concurrent.locks.Lock;
5 import java.util.concurrent.locks.ReentrantLock;
6
7 class Resource2{
8 private String name;
9 private int count=1;
10 boolean flag=false;//标记无资源
11 private Lock lock=new ReentrantLock();
12 private Condition con=lock.newCondition();
13 private Condition pro=lock.newCondition();
14 public void set(String name){
15 lock.lock();//锁
16 try {
17 while(flag)//重复判断标识,确认是否生产
18 con.await();//本方等待
19 this.name=name+"....."+count++;
20 System.out.println(Thread.currentThread().getName()+"生产者。。。。"+this.name);
21 flag=true;
22 pro.signal();
23
24 } catch (InterruptedException e) {
25 e.printStackTrace();
26 }
27 finally{
28 lock.unlock();//释放锁的机制一定要执行
29 }
30 }
31 public void out(){
32 lock.lock();
33 try {
34 while(!flag)
35 pro.await();
36 System.out.println(Thread.currentThread().getName()+"消费者。。。。"+this.name);
37 flag=false;
38 con.signal();
39 } catch (Exception e) {
40 // TODO: handle exception
41 }finally{
42 lock.unlock();
43 }
44 }
45 }
46 //生产者
47 public class Producker2 implements Runnable{
48 private Resource2 res;
49 Producker2(Resource2 res) {
50 this.res=res;
51 }
52 @Override
53 public void run() {
54 // TODO Auto-generated method stub
55 while(true)
56 {
57 res.set("商品");
58 }
59 }
60 }
61 //消费者
62 class customer2 implements Runnable{
63 private Resource2 res;
64 customer2(Resource2 res){
65 this.res=res;
66 }
67 @Override
68 public void run() {
69 while(true){
70 res.out();
71 }
72 }
73 }
74 class OutInt3{
75
76 public static void main(String[] args) {
77 Resource2 r=new Resource2();
78 // new Thread(new customer(r)).start();
79 // new Thread(new Producker(r)).start();
80 customer2 n1=new customer2(r);
81 Producker2 o1=new Producker2(r);
82 Thread t1=new Thread(n1);
83 Thread t2=new Thread(o1);
84 Thread t3=new Thread(n1);
85 Thread t4=new Thread(o1);
86 t1.start();
87 t2.start();
88 t3.start();
89 t4.start();