_____多线程_____
线程与进程
进程:
一个正在执行的程序,最少包含一个线程
线程:
程序中的执行路径(控制单元)
很好的例子就是迅雷多线程下载,下载同一个文件的时候有N个节点在同时下载,其中的节点就相当于线程,执行中的迅雷就相当于进程。class Demo {
public static void main(String[] args) {
System.out.println("main");
}
}
上面这个main方法在执行时就是一个线程,它的执行路径就是main方法中的顺序代码
其实JVM的运行的时候就是多线程,还有垃圾回收机制的线程等等
创建多线程
1.继承Thread类:
class PrimeThread extends Thread {
//重写run方法,其中的代码就是此线程的执行路径
//类似于我在面向对象-继承里面见的模板
public void run() {
for(int i=0; i<100; i++)
System.out.println("PrimeThread run:"+i);
}
}
如何使用呢?调用start()方法,它会启动线程并且执行run()方法中的代码
class Demo {
public static void main(String[] args) {
new PrimeThread().start();
for(int i=0; i<100; i++)
System.out.println("Demo run:"+i);
}
}
结果会出下类似如下片段:
Demo run:88
PrimeThread run:80
Demo run:89
PrimeThread run:81
照成这种现象的原因在单核CPU中是因为同一时间只能有一个线程抢占到CPU资源让CPU去运行它,所以Demo和PrimeThread所属的线程在CPU中不断地轮流执行照成的,多核处理器还有可能是因为输出限制照成。单线程的话就不可能出现这种现象
不能直接调用run()方法,直接执行等于主线程直接进入到run()方法中无顺序执行,执行完毕后又直接出来,还是单线程,所以必须调用start()来让系统创建一个新线程并且在新线程中执行run()中的代码.
下面我先说说线程的几种状态:
线程也有名称,Thread里面有name成员变量,可以通过提构造方法或者setName方法设置,通过getName方法取得,如取得当前线程名称:
Thread.currentThread().getName();
2.实现Runnable接口:
class PrimeThread impements Runnable {
public void run() {
System.out.println("PrimeThread run");
}
}
class Demo {
public static void main(String[] args) {
PrimeThread pt = new PrimeThread();
new Thread(pt).start();
new Thread(pt).start();
}
}
此方式能够让多个线程调用同一对象的run()方法
相对于继承创建线程,此方法能够实现同时操作同一对象的数据,而继承方法每开启一个线程都要重新创建一个对象,是多个线程运行这多个对象的run方法。
最简单的就是一个售票的例子:
class Ticket implements Runnable {
private int tick = 100;
public void run() {
while(tick >0)
System.out.println(tick--+":号已出售票,出票口:"+Thread.currentThread().getName());
}
}
class Demo {
public static void main(String[] args) {
Ticket pt = new Ticket();
new Thread(pt).start();
new Thread(pt).start();
new Thread(pt).start();
}
}
但是这个程序有个问题,就是假如还剩最后一张票,当其中一个线程刚好进入while循环准备售票时,cpu切换到了另外一个线程也进入了while循环并且出售了此票,然后cpu又切换回来,那么就会出现0张剩余的时候继续售出了一张票,这就是多线程下的数据安全问题,这时候就需要引入锁的概念了:
线程安全
1.synchronized的使用:
同步代码块:
修改上面的代码class Ticket implements Runnable {
private int tick = 100;
public void run() {
synchronized(this) {
while(tick >0)
System.out.println(tick--+":号已出售票,出票口:"+Thread.currentThread().getName());
}
}
}
其中synchronized所包含的便是我们需要同步的代码,参数需要选择执行时都能访问唯一对象当作锁。
打个比方,synchronized代码块就像火车上的卫生间,火车就是进程,人就是线程,当一个人进入卫生间时就会关上门并且使用厕所这个资源,其他的人就不能进来了,必须等到里面的人出来才能进去~
同步函数:
class Ticket implements Runnable {
private int tick = 100;
public void run() {
while(tick >0)
sale();
}
public synchronized void sale() {
System.out.println(tick--+":号已出售票,出票口:"+Thread.currentThread().getName());
}
}
可以直接把synchronized当成方法修饰符来用,代表此方法一次只能一个线程进入,当然有人问为什么不直接修饰run()方法,如果修饰了run()方法,那么就必须得等到一个线程执行完run()方法才能轮到其他线程,在此程序中就等于一个线程买完所以的票才会释放,达不到多线程目的。
其实同步函数就等于:
public void sale() {
synchronized(this) {
System.out.println(tick--+":号已出售票,出票口:"+Thread.currentThread().getName());
}
}
使用的this当前对象为锁,若方法为static静态函数,那么是以当前类的字节码.class为锁
synchronized(Ticket.class)
死锁:
若存在嵌套锁就有可能会引发死锁的问题,这里我们写一个死锁的例子:class PrimeThread implements Runnable {
public boolean flag = true;
public void run() {
if(flag) {
while(true)
synchronized(this) {
System.out.println("if this");
synchronized(PrimeThread.class) {
System.out.println("if class");
}
}
} else {
while(true)
synchronized(PrimeThread.class) {
System.out.println("else class");
synchronized(this) {
System.out.println("else this");
}
}
}
}
}
class Demo {
public static void main(String[] args) {
PrimeThread pt = new PrimeThread();
new Thread(pt).start();
try{Thread.sleep(10);}catch(Exception e){}
pt.flag = false;
new Thread(pt).start();
}
}
这样就会出现互等释放的情景,照成死锁,学会如何避免死锁是一个经验的累积。
线程的等待唤醒
wait():当前线程等待notify():唤醒在此对象监视器上等待的单个线程
notifyAll():唤醒在此对象监视器上等待的所有线程
以上方法为Object的方法,适用于旧版本的锁机制,使用锁锁对象来调用,下面我们只讲解1.5版本后的新方法。
Lock与Condition的使用:
JDK1.5以后出现的更加灵活的锁,Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。区别:
老版本的锁对应一个监视器,又要针对于生产者和消费者的模型来实现分别唤醒和等待就必须要在建锁,两个锁一旦形成嵌套就容易照成死锁。就算使用一个监视器,对于多个生产者和消费者同时运行时也必须每次唤醒此监视器下的所有线程,额外占用CPU资源。
新版本的锁可以对应多个监视器,实现了分类唤醒。
下面是基于新版本锁的生产者和消费者模型:
class Resource {
private Lock lock = new ReentrantLock();
private Condition putCond = lock.newCondition();
private Condition getCond = lock.newCondition();
private int count = 10;
private ArrayList items = new ArrayList();
public void put(Object o) {
lock.lock();
try{
while(items.size() == count)
putCond.await();
items.add(o);
getCond.signalAll();
} finally {
lock.unlock();
}
}
public Object get() {
lock.lock();
try{
while(items.size() == 0)
getCond.await();
items.remove(items.size()-1);
putCond.signalAll();
} finally {
lock.unlock();
}
}
}
停止线程
1.5版本后,stop等强制停止方法过时不适合继续使用,停止线程的方法就只剩下一个run方法结束了,对于大多数线程程序来说都是循环的,所以我们只需要控制住循环就可以结束线程,对与线程中没有受阻方法如:wait()、join()、sleep(long)包括Condition中的await等等,可以通过控制循环判断条件来来停止线程。否则就要通过Thread类的interrupt方法来临时唤醒线程并且抛出InterruptedException异常来处理结束线程,实例如下:
class Demo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
t1.start();
t1.interrupt();
}
}
class StopThread implements Runnable {
private Lock lock = new ReentrantLock();
private Condition cond = lock.newCondition();
public void run() {
while(true) {
lock.lock();
try {
cond.await();
}catch(InterruptedException e){
System.out.println(e);
//当捕获到InterruptedException异常时,我们就直接跳出循环结束run方法
break;
} finally {
lock.unlock();
}
}
}
}
守护线程或用户线程
与普通线程在运行时没有任何区别,唯一的区别在结束,当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
class Demo {
public static void main(String[] args) {
System.out.println("main...run");
StopThread st = new StopThread();
Thread t1 = new Thread(st);
//t1.setDaemon(true);
t1.start();
System.out.println("main...over");
}
}
class StopThread implements Runnable {
public void run() {
while(true) {
System.out.println("t1...run");
}
}
}
这个程序执行后,Java虚拟机进程程序会一直保持执行t1线程而不退出,程序没有结束。
当我们把t1线程通过t1.setDaemon(true)设置为守护线程时,当程序主函数所在的线程main执行完毕只剩下t1线程时,无论t1线程是否中断和执行,进程都会终止。
Join()方法
理解:
当前线程等待调用join()方法的线程执行结束才继续执行
class Demo {
public static void main(String[] args) {
JoinThread jt = new JoinThread();
Thread t1 = new Thread(jt);
Thread t2 = new Thread(jt);
t1.start();
//此处的join调用时,当前线程即主线程放弃执行权等待t1线程执行结束再继续执行主线程
//try{t1.join();}catch(Exception e){}
t2.start();
//此处的join调用时,由于现在t2线程已经开始,所以当前线程即主线程放弃执行权等待t1线程执行结束,
//t1线程和t2线程同时抢夺CPU资源,当t1执行结束时,无论t2是否结束,主线程加入到抢夺cpu资源队列
//try{t1.join();}catch(Exception e){}
System.out.println("main...over");
}
}
class JoinThread implements Runnable {
public void run() {
for(int i=0;i<60;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
yield()方法
释放CPU执行权,重新加入等待执行队列
线程优先级
优先级为1-10,默认为5,表现为CPU执行其的频率,10为最高通过setPriority(int newPriority)来设置线程的优先级,允许在线程开始后设置
最后
在实际开发中,一般使用匿名类来完成线程封装1.
new Thread() {
public void run() {
//代码段
}
}.start();
2.
Runnable r = new Runnable() {
public void run() {
//代码段
}
}
new Thread(r).start();