------- android培训、java培训、期待与您交流! ----------
黑马程序员---多线程
进程
进程,是一个正在执行中的程序。启动一个程序(比如QQ,Word),就是打开一个进程。查看任务管理器,可以发现有多个进程在同时执行,其实但实际是“由于CPU 分时机制的作用,使每个进程都能循环获得自己的CPU 时间片。但由于轮换速度非常快,使得所有程序好象是在‘同时’运行一样。‘线程’是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。”(《Thinking in java》)。进程也是一种封装形式。
线程
每个进程中至少有一个线程,线程是进程中的一个独立的控制单元,线程在控制着进程的执行。Java VM启动的时候就会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行,而且这个线程的运行代码存在于主函数当中,该线程称之为主线程。(其实,虚拟机启动的时候就是多线程的,因为除了主线程,还有一个负责垃圾回收的线程。)
多线程存在的意义
很明显,多线程可以提高运行效率。多线程一个重要的应用就是建立反应灵敏的用户界面。
线程创建的方式
1. 继承Thread类;2.实现Runnable接口。
多线程的特性
1.随机性:多个线程交替执行,执行结果随机。
线程创建方式一:继承Thread类
创建步骤:
1.继承Thread类;
2.复写Thread类中的run方法;
目的是将自定义的代码存储在run方法中,让线程运行。
3.调用线程的start方法,该方法有两个作用---启动线程和调用run方法。
发现运行结果每一次都不同,因为多个线程都在获CPU的执行权,CPU执行到谁,谁就运行。但要明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象地把多线程的运行行为描述为在互相抢夺CPU的执行权。这就是多线程的一个特性---随机性,谁抢到谁执行,执行多久由CPU决定。
为什么要覆盖run方法?Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。也就是说Thread类中的run方法,是用于存储线程要运行的代码的。
示例代码:
/*
练习:
创建连个线程,和主线程交替运行。
原来线程都有自己默认的名称。
thread-编号,编号从0开始。
staticThread currentThread():获取当前线程对象的引用
getName():获取线程的名称
设置线程的名称:setName()或者构造函数。
*/
classTest extends Thread
{
//private String name;
Test(String name)
{
//this.name = name;
super(name);
}
public void run()
{
for(int x =0; x<60;x++)
{
System.out.println((Thread.currentThread()==this)+"...."+this.getName()+" is running----"+x);
//System.out.println(this.getName()+" is running----"+x);
}
}
}
classThreadTest
{
public static void main(String[] args)
{
Test t1 = new Test("one");
Test t2 = newTest("two");
t1.start();
t2.start();
//t1.run();//没有启动线程,仅仅是调用run方法。
//t2.run();
for(int x = 0;x<60;x++)
{
System.out.println("mainis running----"+x);
}
}
}
线程的运行状态
Thread类内部有个public的枚举Thread.State,里边将线程的状态分为:
NEW-------新建状态,至今尚未启动的线程处于这种状态。
RUNNABLE-------运行状态,正在 Java 虚拟机中执行的线程处于这种状态。
BLOCKED-------阻塞状态,受阻塞并等待某个监视器锁的线程处于这种状态。
WAITING-------冻结状态,无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
TIMED_WAITING-------等待状态,等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
TERMINATED-------已退出的线程处于这种状态。
示意图:
线程创建方式二:实现Runnable接口
创建步骤:
1. 定义类实现Runnable接口
2. 实现Runnable接口中的run方法。
将线程要运行的代码存放在该run方法中。
3. 通过Thread类建立线程对象。
4. 将Runnable接口的子类作为实际参数传递给Thread类的构造函数。
为什么这样做?因为自定义的run方法所属的对象是Runnable的子类对象,所以要让线程去指定对象的run方法,必须明确该run方法所属的对象。
5. 调用Thread类中的start方法,以启动线程并调用Runnable接口子类的run方法。
实现方式和继承方式有什么区别?
1.实现方式的好处:避免了单继承的局限性,在定义线程时,建议使用实现方式。
2.运行代码存放的位置不同:继承Thread线程代码存在在Thread子类的run方法中;实现Runnable,线程代码存放在接口的子类的run方法中。
示例:
classTicket implements Runnable//extendsThread
{
private int tick = 100;//可以用static来说明共享票源,但这时tick生命周期太长。
public void run()
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale..."+tick--);
}
}
}
}
classTicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t0.start();
t1.start();
t2.start();
t3.start();
/*
Ticket t0 = new Ticket();
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
t0.start();
t1.start();
t2.start();
t3.start();
*/
}
}
线程间的安全问题
安全问题产生的原因
当多条语句在操作同一个线程共享数据时,一个线程的多条语句只执行了一部分,还没有执行完,当另一个线程参与进来执行,就会导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中,其他线程不可以参与执行。即实现同步。
实现同步有两种方式:同步代码块和同步函数。
只要同步,都有锁。
同步代码块
java对于多线程的安全问题,提供了专业的解决方式,这就是同步代码块。格式如下:
synchronized(对象)
{
需要被同步的代码;
}
同步代码块的原理
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,即使获取了cpu的执行权,也进不去,因为没有锁。
同步的前提
1.必须要有两个或者两个以上的线程;2.多个线程使用同一个锁。
必须保证同步中只能有一个线程在运行。
同步的利弊
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
如何找到需要同步的代码?
1.明确哪些代码是多线程运行代码
2.明确共享数据。
3.明确多线程运行代码中哪些语句是操作共享数据的。操作共享数据的代码即为要同步的代码。
示例1:
classTicket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)
{
if(tick>0)
{
try {Thread.sleep(10);} catch (Exception e){}
System.out.println(Thread.currentThread().getName()+"....sale..."+tick--);
}
}
}
}
}
classTicketDemo2
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
示例2:
/*
需求:
银行有一个金库
有两个储户分别存300元,每次存100。存3次。
目的:改程序是否有安全问题,如果有,如何解决?
如何找问题?
1.明确哪些代码是多线程运行代码
2.明确共享数据。
3.明确多线程运行代码中哪些语句是操作共享数据的。
*/
classBank
{
private int sum;
public synchronized void add(int n)
{
//synchronized(this)
//{
sum = sum + n;
try{Thread.sleep(10);}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"sum="+sum);
//}
}
}
classCus implements Runnable
{
private Bank b = new Bank();
public void run()
{
for(int x = 0;x<3;x++)
{
b.add(100);
}
//System.out.println(Thread.currentThread().getName());
}
}
classBankDemo
{
public static void main(String[] args)
{
Cus c = new Cus();
new Thread(c).start();
new Thread(c).start();
}
}
同步函数:
同步代码块可以抽取为同步函数,以简化书写。同步函数使用的锁是this引用。
验证锁为this的代码
验证非静态同步函数的锁为this
packagebxd.day11;
/*
非静态同步函数用的是哪一个锁?
函数需要被对象调用,那么函数都有i个所属对象的引用,就是this
所以非静态同步函数使用的锁是this
通过该程序来验证
使用两个线程来买票,
一个线程在同步代码块中
一个线程在同步函数中
都在执行卖票动作
*/
classTicket1 implements Runnable//extendsThread
{
private int tick = 1000;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(this)//如果不是this而且其他的(两个前提:多个线程,同一个锁),则会出现票号为0的情况。
{
if(tick>0)
{
try {Thread.sleep(10);} catch (Exception e){}
System.out.println(Thread.currentThread().getName()+"....code..."+tick--);
}
}
}
}
else
{
while(true)
{
show();
}
/*
System.out.println(Thread.currentThread().getName());
ThisLockDemo.java:16: 错误: 无法访问的语句
System.out.println(Thread.currentThread().getName());
^
1 个错误
上句会出现如上错误,为什么?因为while在无限循环,这样写没有意义,会出现编译错误。
*/
}
}
public synchronized void show()
{
if(tick>0)
{
try {Thread.sleep(10);} catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"..show.."+tick--);
}
}
}
classThisLockDemo
{
public static void main(String[] args)
{
Ticket1 t = new Ticket1();
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
t0.start();
try {Thread.sleep(10);} catch(Exception e){}
//这是使用sleep是为了让t0线程使用同步代码块;如果没有这句,可能会出现两个线程都在使用同步函数的情况。
t.flag= false;
t1.start();
}
}
静态同步函数
静态同步函数使用的锁是该函数所属类的class对象(类名.class)。
验证代码,将验证非静态同步函数中的this换成Ticket1.class,show()函数加静态修饰符,即可。
单例设计模式
/*
单例设计模式
饿汉式
classSingle
{
private static final Single s = newSingle();
private Single(){}
public static Single getInstance()
{
return s;
}
}
*/
//懒汉式
classSingle
{
private static Single s = null;
private Single(){}
/*
//这样比较低效
public static synchronized SinglegetInstance()
{
if(s==null)
s = new Single();
return s;
}*/
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
s = new Single();
}
}
return s;
}
}
同步的死锁问题
死锁现象:
所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象:死锁。死锁现象不是一定会发生的,也可能出现和谐状态。死锁现象其实是说,有可能出现死锁的情况。
死锁产生的原因:
同步中嵌套同步,而锁却不同。
死锁程序示例:
packagebxd.day11;
classTest1 implements Runnable
{
private boolean flag;
Test1(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true)
{
synchronized(MyLock.locka)
{
System.out.println("if locka");
synchronized(MyLock.lockb)
{
System.out.println("if lockb");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockb)
{
System.out.println("else lockb");
synchronized(MyLock.locka)
{
System.out.println("elcs locka");
}
}
}
}
}
}
classMyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
new Thread(newTest1(true)).start();
new Thread(newTest1(false)).start();
}
}
线程间的通信
之前的代码多个线程对数据的处理动作都是一样的,如果多个线程对数据的操作不一致,那么这就是线程间的通信。
线程间通信的安全问题
对于一个共享资源,一个线程进行写入操作,另一个线程进行读取操作,那么对于这两个对共享资源进行不同操作的线程,它们当中所有操作(读取和写入)共享资源的代码,需要用一个共同的锁来同步;否则,会出现写入没进行完就被读取或读取没进行完就被写入这两种错误的操作。
对于这个共同的锁,一般使用共享资源对象(当然也可以使用某个类的字节码文件,保证两个线程中使用的是同一个锁即可)。
线程间通信的等待唤醒机制
读取和写入线程中的操作共享资源的代码同步后,发现这两个不同操作的线程仍然有问题:
1. 写入线程,获取执行权后,可能会连续多次写入,而本次写入的数据覆盖了上次写入的数据,这样导致部分写入的数据没有被读取线程读到。
2. 读取线程,获取执行权后,对一个同样的数据可能连续多次读取,这几次都没有取出新数据,产生了无效的读取操作。
这时,就需要用到等待唤醒机制,以使得写入和读取线程依次交替执行。
示例代码1:
packagebxd.day12;
/*
线程间的通讯:
其实就是多个线程在操作同一个资源。
但是操作的动作不同
*/
classRes1
{
String name;
String sex;
boolean flag = true;
}
classInput1 implements Runnable
{
private Res1 r;
Input1(Res1 r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
synchronized(r)
{
if(r.flag)
try{r.wait();}catch(Exceptione){}
if(x==0)
{
r.name ="mike";
r.sex ="man";
}
else
{
r.name ="丽丽";
r.sex = "女";
}
r.flag = true;
try{r.notify();}catch(Exceptione){}
}
x =(x+1)%2;
}
}
}
classOutput1 implements Runnable
{
private Res1 r;
Output1(Res1 r)
{
this.r = r;
}
public void run()
{
while(true)
{
synchronized(r)
{
if(!r.flag)
try{r.wait();}catch(Exceptione){}
System.out.println(r.name+"..."+r.sex);
r.flag = false;
try{r.notify();}catch(Exceptione){}
}
}
}
}
class InputOutputDemo
{
public static void main(String[] args)
{
Res1 s = new Res1();
Input1 in = new Input1(s);
Output1 out = new Output1(s);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
wait()、notify()和notifyAll()
阻塞的线程都放在线程池中。notify唤醒的都是线程池中的线程,通常是线程池中的第一个线程。notifyAll唤醒线程池中的所有阻塞线程。
这三个方法都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。
为什么这些操作线程的方法要定义在Object类中呢?
因为这些方法在操作同步中线程时,都必须要标示它们所操作线程持有的锁。只有同一个锁上的被等待线程可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,而能被任意对象调用的方法定义在Object类中。
对InputOutputDemo的优化:
packagebxd.day12;
classRes
{
private String name;
private String sex;
private boolean flag = true;
public synchronized void set(Stringname,String sex)
{
if(flag)
try{this.wait();}catch(Exceptione){}
this.name = name;
this.sex = sex;
flag = true;
try{notify();}catch(Exception e){}
}
public synchronized void out()
{
if(!flag)
try{wait();}catch(Exceptione){}
System.out.println(name+"......"+sex);
flag = false;
try{notify();}catch(Exception e){}
}
}
classInput implements Runnable
{
private Res r;
Input(Res r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
if(x==0)
r.set("mike","man");
else
r.set("丽丽","女");
x =(x+1)%2;
}
}
}
classOutput implements Runnable
{
private Res r;
Output(Res r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class InputOutputDemo2
{
public static void main(String[] args)
{
Res s = new Res();
new Thread(new Input(s)).start();
new Thread(new Output(s)).start();
}
}
注意:优化后锁都是Res类中的this,时刻记住,wait()、notify、notifyAll()方法操作的是以这些方法所属对象为锁的线程。
生产者消费者示例
代码1:
packagebxd.day12;
classProducerConsumerDemo
{
public static void main(String[] args)
{
Resource1 res = new Resource1();
Producer1 pro = newProducer1(res);
Consumer1 con = newConsumer1(res);
Threadt0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
classResource1
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)
{
while(flag)//if(flag)
try{wait();}catch(Exceptione){}
this.name = name +"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag = true;
this.notifyAll();
}
public synchronized void out()
{
while(!flag)//if(!flag)
try{wait();}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag =false;
this.notifyAll();
}
}
classProducer1 implements Runnable
{
private Resource1 res;
Producer1 (Resource1 res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.set("商品");
}
}
}
classConsumer1 implements Runnable
{
private Resource1 res;
Consumer1(Resource1 res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.out();
}
}
}
注意:
1.为什么要把 if 换成while?因为如果同组的某一线程运行完毕后,标记值已经改变,如果此时同组的另一个线程获取执行资格,本不应该执行,但因为没有判断标记,或导致连续生产或连续消费。使用while就是为了让线程在冻结唤醒获取执行权后,再判断一次标记。
2.为什么要把notify变成notifyAll?因为notify只唤醒一个,如果使得唤醒的是跟他同组的线程时,会导致当前所有组的线程都处于冻结状态。
代码2:使用Lock和Condition
JDK1.5中提供了多线程的升级解决方案:将同步synchronized替换成Lock操作;将Objec(监视器对象)的wait,notify,notifyAll,替换成Condition对象的相应操作,Condition对象可以通过Lock锁进行获取。这是一种显式的锁机制和显式的锁对象上的等待唤醒操作机制,它把等待唤醒动作封装成了Condition类对象,让一个锁可以对应多个Condition对象(监视器对象),即一个锁可以对应多组wait-notify。
packagebxd.day12;
classProducerConsumerDemo
{
public static void main(String[] args)
{
Resource1 res = new Resource1();
Producer1 pro = newProducer1(res);
Consumer1 con = newConsumer1(res);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
classResource1
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)
{
while(flag)//if(flag)
try{wait();}catch(Exceptione){}
this.name = name +"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag = true;
this.notifyAll();
}
public synchronized void out()
{
while(!flag)//if(!flag)
try{wait();}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag =false;
this.notifyAll();
}
}
classProducer1 implements Runnable
{
private Resource1 res;
Producer1 (Resource1 res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.set("商品");
}
}
}
classConsumer1 implements Runnable
{
private Resource1 res;
Consumer1(Resource1 res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.out();
}
}
}
注意:
1.在上边的实例中,实现了本组线程只唤醒另外一组线程的操作;
2.升级方案在使用异常处理时,Lock类的unlock方法一定要执行,因此放在了finally中。
停止线程、守护线程和Join方法
停止线程
怎么停止线程?
Thread类中提供有stop方法,但该方法已经过时了。要停止线程只有一种情况:run方法结束。开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。
特殊情况:
当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这是需要对冻结状态进行清除。
强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。Thread类中提供了该方法 interrupt()。
守护线程(或叫后台线程)
守护线程,其实就是后台线程(我们一般能看到的线程都是前台线程)。后台线程的特点是,开启后和前台线程一起抢夺cpu资源,它的开启运行跟前台线程都没有区别,只是结束的时候有区别:当所有的前台线程都结束后,后台线程会自动结束。Thread类中,把一个线程标记为守护线程使用setDaemon()方法,注意要它在线程启动前设置。主线程是前台线程。如果线程A依赖于另一个线程B,那么A就可以设置为B的守护线程,在B中设置调用A的setDaemon(true),例如输出依赖于输入的情况(InputOutPutDemo)。
示例代码:
classStopThread implements Runnable
{
private boolean flag = true;
public synchronized void run()
{
while(flag)
{
try
{
wait();
}
catch (InterruptedExceptione)
{
System.out.println(Thread.currentThread().getName()+"------Exception");
/*
对于处于冻结状态的线程,这样做就可以免去调用changeFlag方法。
当既有冻结线程,又有运行状态的线程时,changeFlag方法和interrupt方法都需要使用。
*/
flag = false;
}
System.out.println(Thread.currentThread().getName()+"------run");
}
}
public void changeFlag()
{
flag = false;
}
}
classStopThreadDemo
{
public static void main(String[] args)
{
StopThread st = new StopThread();
//不能把把主线程设置为守护线程,因为设置动作要在线程启动之前,而主线程已经启动了。
// Thread.currentThread().setDaemon(true);
Thread t0 =new Thread(st);
Thread t1 =new Thread(st);
//把线程设置为守护线程,当前台线程运行完后,守护线程会自动结束
t0.setDaemon(true);
t1.setDaemon(true);
//
//线程启动
t0.start();
t1.start();
int num = 0;
while(true)
{
if(num++==60)
{
//通过设置标记结束正在运行的线程
// st.changeFlag();
//通过interrupt方法将冻结状态的线程唤醒,注意名字虽然叫interrupt,但不是说中断线程,而是说中断当前的冻结状态,是一种异常动作,所以会抛出InterruptException。
t0.interrupt();
t1.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"------main--"+num);
}
System.out.println("over!");
}
}
Join方法
当A线程执行到了B线程的.join()方法时,A就会等待,等B线程都执行完,A才执行。
join用来临时加入线程执行。
示例代码:
classDemo implements Runnable
{
public void run()
{
/*
Thread d1=new Thread(new Demo1());
try {
d1.start();
d1.join();
} catch (Exception e) {
}
*/
for(int x = 0;x<70;x++)
{
System.out.println(Thread.currentThread().getName()+"---Demo-----"+x);
}
}
}
/*
classDemo1 implements Runnable
{
public void run()
{
for(int x = 0;x<70;x++)
{
System.out.println(Thread.currentThread().getName()+"---Demo1--------"+x);
}
}
}*/
classJoinDemo
{
public static void main(String[] args)throws Exception
{
Demo d = new Demo();
Thread t0 = new Thread(d);
Thread t1 = new Thread(d);
t0.start();
// t0.join();//此时只有两个前台线程,t0和主线程,t0调用join方法后,主线程等待直到t0执行结束。
t1.start();
t0.join();//此时有三个前台线程,t0和主线程,t0调用join方法后,主线程等待直到t0执行结束才执行,t0与t1抢夺cpu执行权。
for(int x = 0;x<300;x++)
{
System.out.println("main--------"+x);
}
System.out.println("over!");
}
}
------- android培训、java培训、期待与您交流! ----------