多线程
概念
进程:是一个正在执行的程序,每一个进程都有一个执行顺序,或者叫一个控制单元。
线程:就是进程中一个独立的控制到元,线程在操控着进程的的执行,一个进程中至少有一个线程。
多线程:一个程序里边有多条执行路径,就是说由程序是由多个线程组成的,多线程虽然降低了程序运行的效率,但一个程序中线程越多,获取到cpu执行权的概率就越大。
创建线程的方式
创建线程的方式有两种,一种是继承Thread类,另一种是实现Runnable接口。
继承Thread类
步骤:1,定义一个类继承Thread;
2,复写Thread类中的run( )方法;
3,调用线程的start( )方法,开启线程,并调用执行run( )方法。
我们用一个小程序来说明:
class Demo extends Thread
{
public void run()
{
for(int x=0; x<60; x++)
System.out.println("demo run----"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();//创建好一个线程。
d.start();//开启线程并执行该线程的run方法。
//d.run();仅仅是对象调用方法。而线程创建了,并没有运行。
for(int x=0; x<60; x++)
System.out.println("Hello World!--"+x);
}
}
发现运行结果每一次都不同,因为多个线程都在获取cpu的执行权,cpu执行到谁,谁就运行,需要明确的是,在某一时刻,只有一个程序在运行(多核除外),cpu在做着快速的切换,,以达到看上去是同时在运行的结果,我们可以形象的把多线程的运行形容为,多个线程在抢夺cpu的执行权,这就是多线程的一个特性,随机性。谁抢到谁执行,至于执行多长时间,cpu说了算。
多线程run方法和start方法的区别
Thread类用于描述线程,该类定义了一个能用于储存线程运行代码的方法,这个方法就是run方法,也就是说Thread类中的run方法用于存储线程要运行的代码,主线程要运行的代码放在main方法中,虚拟机定义的。我们复写run方法的目的就是将自定义的代码放在run方法中,让线程运行。
d.start():开启线程并执行该线程的run方法;
d.run():仅仅是对象调用方法,线程创建了却并没有运行。
实现Runnable接口
步骤:1,定义一个类实现Rannable接口;
2,覆盖Rannable接口中的run( )方法;
3,通过Thread类创建线程对象;
4,将Rannable接口的子类对象作为实际参数传递给Thread类的构造函数;
5,调用Thread类的start方法开启线程,调用Runnable接口子类的run方法 。
为什么要将Runnable接口的子类对象传递给Thread类的构造函数?
因为自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属的对象。
实现方式和继承方式的区别:我们用一个售票的例子来进行说明,用继承Thread类的方法创建线程的话,需要建立四个线程对象,运行四次,等于是卖了400张票,我们可以想到用静态,但是静态的生命周期太长,所以我们可以选择实现Runnable接口的方法来完成,代码如下:
/*
需求:简单的卖票程序。
多个窗口同时买票。
*/
class Ticket implements Runnable//extends Thread
{
private int tick = 100;
public void run()
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();//只需要建立一个对象,然后将这个对象传递给Thread类的构造函数;
Thread t1 = new Thread(t);//创建了一个线程;
Thread t2 = new Thread(t);//创建了一个线程;
Thread t3 = new Thread(t);//创建了一个线程;
Thread t4 = new Thread(t);//创建了一个线程;
t1.start();
t2.start();
t3.start();
t4.start();
}
}
实现方式和继承方式的区别
实现方式好处:避免了单继承的局限性,在定义线程时,建立使用实现方式。
两种方式区别:继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
线程运行状态
如图所示,线程存在五种状态,分别是创建,运行,临时,冻结和消亡状态。
获取线程对象及名称
currentThread:这是一个静态方法,用于获取当前对象;
getName:获取线程的名称;
setName:设置线程的名称。
多线程的安全问题
增加窗口后的运行结果:
通过上边的买票小程序,我们发现当买票的窗口增加后,可能会出现打印0,-1,-2这些错票的情况,为了验证,我们可以让程序在打票之前,用sleep方法让进入if语句的线程停一段时间,我们队if语句中的代码作如下改动,如图:
分析一下出现这种问题的原因:在if语句中有可能,四个线程进来后,都停在sleep这条语句这里,我们假设现在tick=1,某一个线程获取执行权后,向下执行输出语句,执行完tick--,tick=0,这是,还在if语句中的其他线程已经判断过条件了,所以即使tick的值已经不满足if的条件表达式,这些线程还是会向下执行输出语句,所以就会出现打印出错票的情况,线程越多,有可能打印出的错票就越多。
得出一个结论:当多条语句在执行同一个线程共享数据时,一个线程对象对于语句只执行了一部分,没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:对于多条操作共享数据的语句,只能让一个线程都执行完后在执行其他线程,执行过程中,其他线程不能参与执行。
java对多线程安全问题提供了专门的解决方式:同步代码块和同步函数。
同步代码块
synchronized(对象) //这个对象可以是任意对象;
{
要被同步的代码;
}
那些代码需要被同步,就看那些代码在操作共享数据。
对于售票这个例子,加上同步代码快后的代码如下,就只有run方法的代码有变化,其他地方都不变,在这里为了说明问题,我只写run方法中的代码:
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--);
}
}
}
}
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,及时获取了cpu的执行权,也进不去,因为没有获取锁。
同步的前提:1,必须要有两个或者两个以上的线程;
2,必须是多个线程使用同一个锁;
必须保证同步中只有一个线程在运行。
同步的好处与弊端
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
同步函数
同步函数:就是在函数中添加了synchronized修饰符的函数。
如何找多线程中的安全问题?
1,明确哪些代码是多线程运行代码;
2,明确共享数据;
3,明确多线程运行代码中哪些语句是操作共享数据的。
同步代码快的锁是任意对象,那么同步函数的锁又是什么呢?
对于同步函数的锁,我们可以去验证一下,创建两个线程,一个线程执行同步代码快中的代码,另一个线程执行同步函数中的代码,这个可以通过定义标记来实现,验证的原理就是同步的前提:必须是多个线程使用同一个锁;通过运行结果,我们发现,当同步代码快中的对象是Object类的对象时,还是会发生安全问题,这说明两个锁不一样,换成this后,安全问题就解决了,说明同步函数的锁是this。
如果同步函数被静态修饰后,使用的锁是什么呢?
我们也可以用上面的方法进行验证,发现不再是this,因为静态方法中也不可以定义this;静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象,类名.class 该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
单例设计模式中的懒汉式在多线程,处理安全问题的用的就是用的静态同步函数。
死锁
同步中嵌套同步,而锁却不同。
/*
死锁
*/
class Dead implements Runnable
{
private boolean flag;
Dead(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
synchronized(MyLock.locka)//a锁
{
System.out.println("if locka");
synchronized(MyLock.lockb)//b锁
{
System.out.println("if lockb");
}
}
}
else
{
synchronized(MyLock.lockb)//b锁
{
System.out.println("if lockb");
synchronized(MyLock.locka)//a锁
{
System.out.println("if locka");
}
}
}
}
}
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockDemo
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Dead(true));
Thread t2 = new Thread(new Dead(false));
t1.start();
t2.start();
}
}
出现这种结果:
死锁就相当于吃饭时用筷子,筷子只有一双,两个人每人手里有一根,这时,两人一人一根筷子,但是两人都不愿意让别人先吃,不愿意将筷子送给对方,于是两人都吃不了饭,死锁也是一样,两个线程都想获取对方的锁,如果都这么僵持着,就会出现死锁,死锁就是在这样的情况下造成的。
线程间通信
线程间通信其实就是多个线程在操作同一个资源,但操作的动作不同。
我们用一个小程序来提现什么是线程间通信:这个小程序我们只创建两个线程,已解决过安全问题,而且得到的结果是存一个打一个。
/*
线程间通信
这个小程序,创建了两个线程,一个线程负责设置名字,另一个线程负责打印名字。
*/
class Resource
{
private String name,sex;
private boolean flag;//定义一个标记;
public synchronized void setName(String name,String sex)
{
if(flag)//如果这个标记为真
try{this.wait();}catch(Exception e){}//让线程释放执行权;
this.name = name;
this.sex = sex;
flag = true;//传完值后,把标记改为真;
this.notify();//唤醒后,再回去判断标记久为真,那么,就会wait;
}
public synchronized void printName()
{
if(!flag)//如果这个标记为假
try{this.wait();}catch(Exception e){}//让线程释放执行权;
System.out.println(name+"............"+sex);//如果为假,就打印
flag = false;//打印完,把标记改为假;
this.notify();//唤醒另一个线程后,执行完再回去判断标记if中的条件表达式为真,就会wait;
}
}
class Input implements Runnable
{
private Resource res;
Input(Resource res)
{
this.res = res;
}
public void run()
{
int x = 0;
while(true)
{
if(x==0)
res.setName("BaiYun","M M");
else
res.setName("黑土","G G");
x = (x+1)%2;
}
}
}
class Output implements Runnable
{
private Resource res;
Output(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.printName();
}
}
}
class InputOutputDemo
{
public static void main(String[] args)
{
Resource res = new Resource();
new Thread(new Input(res)).start();//开启并执行线程
new Thread(new Output(res)).start();
}
}
结果是:
在操作同一种资源时,对于不同的处理方式,可以用多线程来完成,为了让两个线程交替执行,可以定义一个标记,在一个线程执行完后,让该线程进入等待状态,同时,唤醒其他在等待中的线程,这就是我理解的等待唤醒机制。
等待中的线程临时存放在线程池中,当我们notify的时候,唤醒的是线程池中的线程,如果有多个线程在线程池中,通常先唤醒第一个在等待的,如果要唤醒很多,可以用notifyAll()方法。
wait和notify方法,是从Object中继承的方法,全都用在同步中,这时必须要指出wait所操作的那个线程所属的锁,wait方法会抛出异常。
为什么这些操作线程的方法都定义在Object类中?
因为这些方法在操作同步中线程时,都必须要标识他们所操作线程持有的那个锁,只有同一个锁上的被等待县城可以被一个锁上的notify唤醒,也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象所调用的方法定义在Object类中。
生产者消费者
对于多个生产者,消费者,在多线程运行时,出现的问题,我们通过下面的代码进行说明,网页上的代码写太多注释,有时候会出问题,所以,我把分析写在后边。
class Resource
{
private String name;
private int count = 1;
private boolean flag;
public synchronized void set(String name)
{
if(flag)//这里用的是if语句来判断标记;
try{wait();}catch(Exception e){}//t1,t2
this.name = name+"-----"+count++;
System.out.println(Thread.currentThread().getName()+"....生产者......"+this.name);
flag = true;
this.notify();//这里只唤醒了一个线程;
}
public synchronized void out()
{
if(!flag)
try{wait();}catch(Exception e){}//t3,t4
System.out.println(Thread.currentThread().getName()+"..........消费者.........."+this.name);
flag = false;
this.notify();
}
}
class Pro implements Runnable
{
private Resource res;
Pro(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.set("+商品+");
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.out();
}
}
}
class ProCon
{
public static void main(String[] args)
{
Resource res = new Resource();
Pro pro = new Pro(res);
Consumer con = new Consumer(res);
Thread t1 = new Thread(pro);//我们创建四个线程来说明问题;
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行后出现了这样的问题:
出现了生产一个商品却被消费了两次的情况,当然,也有可能出现生产两次,消费一次的情况,这是为什么呢?
分析:t1、t2是生产者线程,t3、t4是消费者线程,我们假设t1先获取到执行权,这时标记为false,t1向下执行,生产一个商品,然后标记被设置为true,回来再读标记,t1进入等待状态,释放了执行权;现在t2、t3、t4都有可能获取到执行权,假设t2获取到了执行权,标记为true,它也进入等待状态,释放了执行权;接下来t3、t4中有一个获取到执行权,执行后消费了一次,标记被设置为假,t3进入等待状态,释放执行权;然后t3唤醒了t1,t1生产一个商品后,并没有去唤醒消费者线程,把t2唤醒了,这样,虽然标记已经被t1设置为true,t2它已经判断过标记,被唤醒后直接向下执行,所以又会生产一个商品,于是就出现了生产两次,消费一次的情况,原因就是t1在执行完后,唤醒了本方的线程。
对于这种问题我们应该怎么处理呢?
发生这个现象就是因为没有循环判断标记,所以,可以用while循环去循环判断标记,但是,新的问题出现了,while循环判断标记可能会造成所有线程都陷入等待状态,如图:
那么,我们就需要在线程释放执行权之前,将其他线程都唤醒,可以使用notifyAll()方法,这样问题就解决了。
然而,在开发中这么做是比较麻烦的,所以我们就想:能不能每次唤醒只唤醒对方的线程呢?答案是可以,java在JDK1.5之后给我们提供了 一些新特性,在java.util.concurrent.locks包中,有Condition和Lock接口,还有ReenTrantLock类给我们提供了方法,Lock替代了synchronized方法和语句的使用,Condition替代了 Object监视器方法的使用。接下来,我们就来使用这些新特性,将生产者消费者的代码重新体现。
import java.util.concurrent.locks.*;//包就不一个一个导了
class Res
{
private String name;
private int count;
boolean flag;
private Lock lock = new ReentrantLock();//ReentrantLock是非抽象的,要建立它的对象,调用这些方法。
private Condition condition_pro = lock.newCondition();//获取condition对象,一个锁上可以有多个相关的condition;
private Condition condition_con = lock.newCondition();//获取condition对象
public void set(String name)throws InterruptedException
{
lock.lock();//synchronized同步换成了两个方法,拿锁,释放锁;
try
{
while(flag)
condition_pro.await();//wait换成了await方法
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"......生产者......"+this.name);
flag = true;
condition_con.signal();//notify换成了signal方法;
}
finally
{
lock.unlock();//释放锁的动作一定要做;这里没有catch处理,下边是一样的;
}
}
public void out()throws InterruptedException
{
lock.lock();
try
{
while(!flag)
condition_con.await();
System.out.println(Thread.currentThread().getName()+"............消费者..........."+this.name);
flag = false;
condition_pro.signal();
}
finally
{
lock.unlock();
}
}
}
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while(true)
try
{
r.set("+商品+");//run方法里只能try
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while(true)
try
{
r.out();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
class ProConDemo
{
public static void main(String[] args)
{
Res r = new Res();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
在jdk1.5版本后提供了显示的锁机制,以及显示的锁对象上的等待唤醒操作机制。
Lock:替代了Synchronized。
lock():上锁;
unlock():释放锁;
newCondition():获取Condition对象,一个锁可以对应多个Condition,而以前一个wait只能对应一个notify;
Condition:替代了Object中的wait、notify、notifyAll。
await():相当于wait;
signal():相当于notify;
signalAll():相当于notifyAll。
停止线程
因为stop方法已过时,那么我们想要停止线程要通过什么方法呢?
只有一种,那就是等run方法结束,开启线程运行,运行代码通常都是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。
步骤:1,定义循环标记;
2,使用interrupt中断方法,该方法能结束现成的冻结状态,使线程回到运行状态。
特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束,这是,当没有指定的方式让冻结的线程恢复到运行状态是,需要对冻结状态进行清除,强制让线程恢复到运行状态中来,这样就可以让线程结束,Thread类提供了该方法:interrupt方法,中断线程。
class StopThread implements Runnable
{
private boolean flag =true;
public synchronized void run()
{
while(flag)
{
try
{
wait();
}
catch (InterruptedException e)
{
System.out.println(Thread.currentThread().getName()+"..........Exception");
flag = false;//只要捕捉到异常,说明就有人在强制清除冻结状态;
}
System.out.println(Thread.currentThread().getName()+"....run");
}
}
public void changeFlag()//这个方法就不再需要了;
{
flag = false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while(true)
{
if(num++ == 60)
{
//st.changeFlag();
t1.interrupt();
t2.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"......."+num);
}
System.out.println("over");
}
}
注意:interrupt方法只是将冻结状态线程唤醒,如果没有冻结状态的线程,这个方法是没作用的。
守护线程
特点:1,当正在运行的线程都是守护线程时,java虚拟机退出;
2,该方法必须在县城启动前调用 。
setDemon(boolean):如果参数是true,则将该线程设置为守护线程。
当所有的前台线程都结束,后台线程会自动结束,后台依赖于前台,开启运行都和前台线程没区别,结束时有区别。
应用:比如有一个县城依赖于另一个线程,另外一个线程的数据如果不在运算了,这个线程存在是没意义的,输入线程不输入了,输出线程就不用输出了。
join方法
等待该线程终止。
特点:当A线程执行到了B线程的join方法时,那么A线程就会等待,等B线程都执行完,A线程才会执行,join可以用来临时加入线程执行。
优先级:代表抢资源的频率,所有线程的默认优先级是5,包括主线程。
setPriority(int num) :设置优先级。
yeild方法:暂停当前正在执行的线程对象,并执行其他线程,就是临时释放一下执行权,稍微减缓一下线程执行的频率。