多线程原理
可能都听说过一句话“一个进程有多个线程”。
那什么是进程?什么是线程?简单介绍下,可以帮助对线程介绍时理解
尽管我们看起来设备(电脑、手机)很多任务是同时运行,但是在CPU(单核)中,所有任务在CPU是一个又一个轮流执行的(CPU的速度很快),而这个过程是:加载程序A的上下文,执行程序A,保存程序A的上下文,加载程序B的上下文,执行程序B,保存程序B的上下文......(上下文可暂时理解其为执行前的预备过程以及执行后的善后过程)
根据在上面的描述为基础,我们就可以解释进程和线程了,进程就是:加载上下文+程序执行+保存上下文。程序又很多的代码块(程序段)组成,也就是程序在执行的时候也分成了很多“小任务”去运行共同完成程序的功能,而线程可以指的就是这些“小任务”。可以看出进程是指CPU的时间段,而线程可以指的是CPU更细的时间段,线程注重的是进程内部CPU如何运行。
根据上面的理解,多线程的意思非常清晰了,就是一个进程开启多条线程,而且多条线程并发的在CPU上运行。
并发指的就是单个处理器(单核CPU)对进程多任务同时进行,但是这种处理实际不是真正意义同时,只是处理器在不断切换运行,但是处理器的快速,使得看似同时进行。假设单核CPU并发运行10个线程,每个线程运行处理到切换需要10ms(实际可能更快,现在硬件升级很快),人眨眼的瞬间(0.2S~0.4S),10个线程就已经轮流运行了2~4次,这速度快到让人有了同时运行的错觉。对于单个处理器可以理解为一个资源,在运行的线程会占用这个资源,其他线程没有资源就无法运行。并发对应的是并行。
并行指的就是真正意义的上同时进行,多个处理器(多核CPU)同时运行处理。
咳咳~题外话介绍了这么多,回归正题。
Java是一种多线程语言,并且提出了并发问题,多线程编程的目的就是达到并发效果,所以可以在下文中并发的使用和Java多线程运用粗略的画上等号。
如同android开发中的活动一样,线程也存在生命周期,生命周期如下:
从上图可知,线程的生命周期主要由:新建、准备、运行、死亡、以及阻塞。
新建线程的方法有四种:Thread类、Runnable接口、Callable接口创建线程和Executor框架线程池
1.Thread类和Runnable接口
A.Thread类(API文档:Thread)
创建一个新的类来继承Thread类,然后通过该类的实例化实现线程的创建。继承类必须重写run()方法,该方法是新线程的具体执行内容。类的实例调用start()方法来执行一个新的线程。
通过API文档可以看到,尽管通过继承Thread类来实现多线程也被列为一种多线程实现方式,但是本质上Thread类是实现了Runnable接口。
如下最简单的例程:
class MyThread extends Thread {
public void run( ){
System.out.println("新线程运行成功");
}
}
main中:
MyThread MT1 = new MyThread();
MT1.start();
B.Runnable接口(API文档:Runnable)
从API文档中可以看到,Runable接口需要实现的只有一个方法,run方法。创建新类实现该接口,重写run方法即可,但是线程的启动还是需要Thread类的start()方法。
如下也依旧最简单的例程:
class MyRunnable implements Runnable{
public void run(){
System.out.println("新线程运行成功");
}
}
main中:
MyRunnable MR1 = new MyRunnable( );
Thread MT2 = new Thread(MR1);
MT2.start();
从上面的Thread类和Runnable接口的简要说明和举例可以看出,线程的创建都必须用到Runnable接口的run方法和Thread类的start方法,只是实现的方式不同,就是因为这个不同造成了优缺点上的不同,例如继承Thread类方式相比实现Runnable接口方式多了许多好用的方法,但是却受到了单继承的限制。(PS:可见代码是活的,要活学活用)
从API文档中可看Runnable接口在用法上非常的简单,而Thread类中的构造函数和方法提供了更多的灵活性。
Thread类的构造函数:
Thread()//默认构造器(无线程执行内容、无指定线程命名)
Thread(Runnable runnable)//传入Runnable对象提供内容(有线程执行内容、无指定线程命名)
Thread(Runnable runnable, String threadName)//传入Runnable对象和线程指定名字(有线程执行内容、有指定线程命名)
Thread(String threadName) //传入线程指定名字(无线程执行内容、有指定线程命名)
//下面三个构造函数和上面主要区别在于加入了ThreadGroup线程组的指定,未指定情况的话默认和父线程在同一组线程组
Thread(ThreadGroup group, Runnable runnable)
Thread(ThreadGroup group, Runnable runnable, String threadName)
Thread(ThreadGroup group, String threadName)
Thread(ThreadGroup group, Runnable runnable, String threadName, longstackSize)// 加入了long stackSize指定了线程栈的大小
Thread方法
public void start();和public void run();//上面已经介绍
public static Thread currentThread();//返回当前正在执行的线程对象
public static void dumpStack();//打印此线程的当前堆栈的文本
public static boolean holdsLock(Object x);// 返回当前线程是否在指定对象上具有监视器锁
public void interrupt();//发送中断请求
public final boolean isAlive();//返回线程是否开启并处于运行状态。
public final void join(long millisec);// 设置线程终止的最长时间
public final void setDaemon(boolean on);//
public final void setName(String name);// 设置线程名称
public final void setPriority(int priority);// 更改线程的优先级,优先级参数分别MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY
public static void sleep(long millisec);//指定睡眠时长并进入睡眠
public static void yield();//暂停当前线程对象,并执行其他线程
2.Callable接口创建线程
由上可知,基于Runnable接口创建的线程,执行完成之后不返回值。但是如果我们需要线程完成之后返回一个值,那么可以实现callable接口(API文档:Callable),可以从API文档中看到,需要从Callable接口实现的泛型方法是call(),而不是run( )方法。而Callable接口返回的结果需要Future对象将其参数化,利用get()方法来获取结果,但是在获取结果之前最好用isDone()方法来判断线程是不是执行完成,不然会造成阻塞直到有结果出现。
例程:
public class MyCallable implements Callable
public String call( ){
return "Are you OK?";
}
}
main中:
MyCallable myCallable= new MyCallable ();
FutureTask
myFutureTask = new FutureTask<>(myCallable); //这里为了方便采用了FutureTask类,FutureTask类是Future接口的实现类之一,并且提供了简单方便的构造函数。
new Thread(myFutureTask,"坐等返回值").start();
if(myFutureTask.isDone()){
System.out.println("返回值:"+myFutureTask.get());
}
3.Executor框架
上面三个方法都是一个线程一个线程的创建,当线程多的时候,管理的复杂就体现出来了,在这个情况下,Java为我们提供了一个工具也就是Executor来处理这种问题,Executor在客户端和任务执行之间提供了一个间接层,并由这个中介对象来执行任务。
Executor是一个在线程管理上非常好用的工具,同时也是十分复杂的而且值得研究的工具,这里只是其基础的运用等做介绍(包括这篇文章也是,只是对Java多线程编程进行了概括性并且基础性的介绍,因为多线程编程可以研究的东西实在太多了,甚至可以单独为多线程编程这一块出书了,如:《Java多线程编程核心技术》《Java并发编程实战》,当然在回顾所掌握,写这篇文章的同时,尽量做到较全面性的涉及整个基础框架。)
Executor也有个系列(族谱),如下图:
Executor接口(API文档:Executor)是Executor框架中最基础的部分。同样的,接口里面也只有一个抽象方法需要被实现,也就是execute(Runnablecommand)方法,这个方法用于传入需要被执行的Runnable对象,同时通过API文档可以知道,该接口提供了任务提交与任务运行解耦的方式,意思是将提交任务和执行任务分开,类似于生产者-消费者设计模式,这也是Executor的间接层作用。Executor接口没有实现类只有一个子接口——ExecutorService接口。
ExecutorService接口(API文档: ExecutorService)在Executor接口的基础上定义了更多的方法(功能),例如:提交任务、执行、关闭接收、终止执行等(具体方法看API文档)。可以说Executor接口为线程池提供了最基础的定义,而ExecutorService接口完善了线程池的使用。也说明了为什么除了ExecutorService接口本身以外,Executor接口的子类和子接口都直接或者间接继承至ExecutorService接口。
abstractExecutorService类(API文档: abstractExecutorService)是ExecutorService接口的实现类,在原有的基础上新增了newTaskFor方法,也为他的子类ThreadPoolExecutor类(线程池中非常重要的类)提供了基础,newTaskFor方法作用是输入一个callable对象或者runnable对象,返回一个RunnableFuture对象,也可以说是把callable对象或者runnable对象包装成RunnableFuture对象。
newTaskFor方法:
protected RunnableFuture
newTaskFor (Callable callable) protected RunnableFuture
newTaskFor (Runnable runnable, T value)
ThreadPoolExecutor类(API文档: ThreadPoolExecutor):线程池管理类,此类的构造方法如下:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, longkeepAliveTime, TimeUnit unit, BlockingQueue
workQueue) ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, longkeepAliveTime, TimeUnit unit, BlockingQueue
workQueue,ThreadFactory threadFactory) ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, longkeepAliveTime, TimeUnit unit, BlockingQueue
workQueue,RejectedExecutionHandler handler) ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, longkeepAliveTime, TimeUnit unit, BlockingQueue
workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数介绍:
corePoolSize:核心线程池大小,核心线程即使空闲也会一直存活,当现有线程数小于核心线程数,不管其他线程数状态如何都会优先创建新线程处理。
maximumPoolSize: 线程池最大数量。当任务队列已满,线程数大于核心线程池定义的大小、小于线程池最大数量时,线程池会创建线程来处理任务。若使用了无界任务队列时,这个参数无效。
keepAliveTime: 线程空闲后保持存活时间,当空闲时间达到设定值则会被退出。
TimeUnit: keepAliveTime参数的时间单位
workQueue:任务队列,用于保存等待执行的任务的阻塞队列。有以下几种队列选择:
ArrayBlockingQueue: 一个有界的且有界缓存的等待队列。ArrayBlockingQueue是一个基于数组结构的阻塞队列实现,内部维护着一个定长数组,按FIFO原则进行排序,在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行
LinkedBlockingQueue: 一个无界的且无界缓存的等待队列。LinkedBlockingQueue是一个基于链表结构的阻塞队列,其内部维持着一个由链表构成的数据缓冲队列,运作过程是当生产者往队列传送一个任务数据时,队列会将数据缓存在队列内部,而生产者回立即返回;而且生产者端和消费者端采用了独立的锁来控制数据同步,从而提高整个队列的并发性能,所以效率上高于ArrayBlockingQueue。然而要注意的是使用LinkedBlockingQueue一定要指定容量,不指定的情况下,其容量默认非常之大,很大可能会把系统内存耗尽。静态工厂方法Excutors.newFixedThreadPool()就是使用了这个队列。
SynchronousQueue: 一个无界的且无缓存的等待队列。由于该Queue本身的特性,在添加任务数据之后必须等待另一个线程调用移除后才能继续添加,否则插入操作一直处于阻塞状态,声明SynchronousQueue有两种不同的方式:1公平模式2非公平模式,也就是采用了公平锁和非公平锁的区别。工厂方法Excutors.newCachedThreadPool()就是使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。优先级的判断通过传入的Compator对象来决定;在PriorityBlockingQueue中,不会阻塞生产者,只会在没有可消费的数据时,阻塞消费者。所以生产者的生产速度不能快于消费者的消耗速度,否则将极大可能内存空间耗尽;内部控制线程同步的锁采用的是公平锁。
DelayQueue:队列大小无限制的具有延迟功能的队列。DelayQueue出场使用的机会非常少,主要的功能是在延时上,当时间到达后,消费者才会在队列中获取数据。同上的,DelayQueue的生产者不会被阻塞,只有获取了数据的消费者才会被阻塞。
threadFactory: 线程工厂,用于设置创建线程的工厂接口。
RejectedExecutionHandler: 饱和策略,设置当线程池满了之后采取的策略,默认是AbortPolicy,表示抛出异常,除此之外还有
CallerRunsPolicy:直接用execute()方法的调用线程中运行任务。
DiscardOldestPolicy:丢弃队列里一个最新的任务,并执行当前任务。
DiscardPolicy:不处理,全部丢弃掉。
在ThreadPoolExecutor的API文档中建议我们使用已经预定义同时较为方便的 Executors 工厂方法:Executors.newCachedThreadPool()(无界线程池,带自动线程回收)、Executors.newFixedThreadPool(int)(固定大小的线程池)、Executors.newSingleThreadExecutor()(单个后台线程)。
ScheduledExecutorService接口(API文档: ScheduledExecutorService)
ScheduledExecutorService接口继承于ExecutorService接口,在原有的继承上,为线程池加入了定时任务功能,使得定时任务和线程池功能结合。增加定义了以下几个方法:
abstract ScheduledFuture> schedule(Runnablecommand, long delay, TimeUnit unit): command为任务,delay为延时时间,unit为时间单位,创建并执行在给定延迟后执行的一次性操作。
abstract
ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit): callable为任务(callable对象),delay为延时时间,unit为时间单位,创建并执行在给定延迟后执行的一次性操作。 abstract ScheduledFuture> scheduleAtFixedRate(Runnablecommand, long initialDelay, long period, TimeUnit unit):command为任务,initialDelay为延时时间,period为间隔时间,unit为时间单位,创建并执行一个定期动作,在给定的初始延迟后启用,然后下一次启动的时间和本次启动的时间间隔为delay;如果执行异常,则后续执行被禁止。否则,任务将仅通过取消或终止执行者而终止。如果此任务的任何执行时间超过其时间段,则后续执行可能会迟到,但不会同时执行。
abstract ScheduledFuture> scheduleWithFixedDelay(Runnablecommand, long initialDelay, long delay, TimeUnit unit):command为任务,initialDelay为延时时间,delay为延时时间,unit为时间单位,创建并执行一个定期动作,在给定的初始延迟后启用,当执行终止后开始计时,计时至delay设定间隔时间后开始下一次执行。
ScheduledThreadPoolExecutor类(API文档: ScheduledThreadPoolExecutor)
ScheduledThreadPoolExecutor类继承于ThreadPoolExecutor类同时实现了ScheduledExecutorService接口,将定时功能与ThreadPoolExecutor类功能结合。这个类的构造函数可以参考ThreadPoolExecutor类:
ScheduledThreadPoolExecutor(int corePoolSize): corePoolSize为线程容量,构造一个指定容量的对象
ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory):
ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler):
ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory,RejectedExecutionHandler handler)
参数介绍:corePoolSize为线程容量,threadFactory为线程工厂,handler为饱和策略
Executors类(API文档: Executors)
Executors类的底层是ThreadPoolExecutor类,主要提供了工厂方法用来创建不同类型的线程池,常用的就是在ThreadPoolExecutor类介绍中涉及到的三个:Executors.newCachedThreadPool()(无界线程池,带自动线程回收)、Executors.newFixedThreadPool(int)(固定大小的线程池)、Executors.newSingleThreadExecutor()(单个后台线程)
通过上面的介绍,来写一个非常简单的Executor框架创建线程的例程:
public class myRunable implement Runnable{
public void run(){
System.out.print("myExecutor")
}
}
main中:
ExecutorService myExecutor = Executor. newCachedThreadPool();
for(int i=0 ; i<5 ; i++){ myExecutor.execute(new myRunable( )); }
myExecutor.shutdown();
结束线程
一般结束线程只要run()方法里的内容运行完毕,自然就会结束,或者使用条件语句“令其”run方法运行结束,但是如果一直被阻塞的状态下(没有超时设定下),那么线程就没办法正常的“老死”,这时候只能手动去结束,结束方法有两种:stop方法和interrupt方法,但是stop方法极其不安全,就像台式机电脑突然被断电一样的,所以不介绍也不要使用,知道它曾经出现过就好。而interrupt方法本质是上改变中断状态,如果线程本身是处于由wait()系列方法、join()系列方法、sleep()系列方法所引起阻塞(中断)状态,那么interrupt方法才会去结束线程同时抛出InterruptedException。所以结束线程的最好就是自然结束或者被动的自然结束,之所以Java这样的设定也是出于安全方面上的出发。
线程的同步
线程的同步机制有四个:互斥量(Mutex)、临界区(Critical Section)、信号量(Semaphore)、事件(Event)
互斥量机制(Mutex):这种机制的目的在于一个时刻下只允许一个任务访问共享资源,通常的做法是在代码(共享资源)上加入锁(synchronized)语句,当有任务在执行这些加了锁的代码的时候,这些代码就会和其他试图调用它的任务(线程)产生一种互斥的现象。
临界区机制(Critical Section):希望可以多个线程访问方法,但是只有一个线程访问方法内部的部分代码。对这部分代码进行加锁而分离出来的代码段成为临界区。当有一个线程已经在运行临界区里的代码时,其他线程试图访问改临界区时都会被挂起,但运行临界区的任务运行结束,离开临界区,其他线程才可以抢占。
信号量机制(Semaphore):允许多个并有限的线程同时访问资源,一般出现的场景是一个资源下有多个副本,如电脑室里有一定数量的电脑,该机制内部有一个计数器,记录的是该资源里面现在还能容下多少个线程访问。如电脑室有20台电脑,计数器的初始则为20,当有一个线程访问了该资源(使用一台电脑),计数器就会减一,也就是该电脑室还有19台电脑可以被使用。
事件机制(Event):通过通知操作的方式来保持线程的同步,似乎线程之间有了一个优先级。主要体现在一个线程在处理完任务后,可以主动唤醒另外一个线程开始执行任务。
线程同步的方法:
1.对方法进行锁修饰
public synchronized void Method1(){ … }
注意的是,当修饰的是静态方法时,调用该方法会锁住方法所在的整个类。
2.对方法内部的代码块进行锁修饰
通过synchronized对代码块的锁修饰,在synchronized()中指明对象:
public void Method2(){
…
synchronized(object ){
…
}
…
}
3.使用ReentrantLock类
通过lock方法和unlock方法手动上下锁:
使用一:
private Lock lock = new ReentrantLock( )
public void Method3(){
…
lock.lock();
…//被锁住的部分
lock.unlock();
…
}
在使用一中,lock-unlock在功能上相当于对代码块进行上锁
使用二:
private Lock lock = new ReentrantLock( )
public void Method4(){
lock.lock();
…
…
…
lock.unlock();
}
在使用二中,lock-unlock在形式上相当于对整个方法进行上锁,实际上lock-unlock也是对代码块进行锁定,只不过这个代码块就是方法的全部语句。
4.使用volatile特殊域变量实现线程同步
volatile关键字确保了可视性,当被volatile修饰的代码或者代码块对所有线程都是可见的,相对对其的访问提供了免锁的机制,同时其他线程可以修改其值,但是volatile修饰的代码和代码块必须是“独立”的,修饰的这些代码在运行时不依赖于其他代码和代码块的限制,举个反例:被volatile修饰的代码块在执行的时候依赖于它之前的值,那么volatile将无法工作。再者volatile不提供原子操作(指不会被线程调度机制打断的操作),也不能使用修饰final类型的变量。
5.使用局部变量实现线程同步
在使用局部变量来实现线程同步时,一般会采用ThreadLocal类管理变量,使用默认构造方法创建ThreadLocal类的实例,主要运用下面方法对实例管理。
get() : 返回当前线程副本中的值
initialValue() : 返回当前线程的"初始值"
set(T value) : 将当前线程副本中的值设置为value
(关于“副本”一词将在下文解释)
创建例子:
private static ThreadLocal
my ThreadLocal = new ThreadLocal (){ protected IntegerinitialValue() {
…
}
};
采用ThreadLocal类管理变量时,每个线程使用该变量时,都获得该变量的副本,并在副本上操作, 副本之间相互独立,这样每个线程都能随意修改自己的变量副本,而不会对其他线程产生影响。
可能这时候就有疑问了,为什么线程操作的是ThreadLocal类管理下的变量副本还能实现同步?
其实这里的副本指的只是对象引用上的副本,而非真正意义的完全拷贝(深层拷贝)的副本,也就是说,每个对象引用副本都指向同一个堆中的对象(即内存中的对象)。作为对比,可以暂时把 对象引用拷贝+堆中对象拷贝=深层拷贝 ,而这里的副本只是做到了对象引用的拷贝。
上面四个机制五个方式算是同步中最基础的原理和说明,四种机制与五个方式之间其实不难看出是有五个方式遵循的也是四个机制上的原理。除了这些方式外,当然还有实现同步的方法,但是一般理论都建立于这些之上,如阻塞队列实现同步、wait-notify-notifyall实现同步等,这里暂不再做深入介绍
线程之间的协作(也可以称为‘线程之间的通讯’,我觉得协作一词使用得更好)
在上面,我们介绍了同步,当多个线程需要同一个共享资源的时候,需要交替的步入这个资源,所以我们使用了互斥来解决了这个问题,使得多个线程能够共存,而不会使我们的程序崩溃或者异常。
而线程之间的协作(通讯)要解决的事就是让多个线程可以一起共同去解决某个问题,注重的是线程间的协调,就如产品流水线上,产品每个部分的负责人(负责团队)共同协作去制造出一个完整的产品,注重的是彼此的协调,不然每个负责人(负责团队)作出来的那一部分根本和其他部分不兼容,就无法共同完成这个产品。
两个线程间的协调,实际上体现在一个线程内部任务和另外一个线程内部任务之间的联系,这种联系可以称作做线程(任务)之间的握手,而实现这种握手的主要方式有:同步、while轮询的方式、wait/notify机制(Lock/condition)、管道通信
1.同步:
这里的同步指的是通过上文同步的方式去实现线程之间的协作,我们在上文介绍同步的时候就已经隐约可以看出,所有线程对于共享资源的同步不凡也是一种建立联系的方式,如在"使用局部变量实现线程同步"中,关于ThreadLocal类副本解释中提到了,拷贝的副本只是对象引用,这些引用全部指向同一个堆中的对象(同一个内存内容),那么可以看出这个堆对象就是这些线程之间的联系。也许不是特别好理解,举个例子:某个局部变量(ThreadLocal类管理的)是线程A、B、C进行某些操作的条件语句,变量默认值为0,当线程A获取这个变量并且判断为0时,线程A开始执行任务,完成后赋值1,线程B获取这个变量并进行判断为1时开始执行任务,完成后赋值……
通过上面例子就可以很清楚明白通过同步实现线程之间协作的基本逻辑。下面在附上最简单也最常见的通过synchronized关键字来实现线程之间协作的代码,能清楚逻辑即可。
public class myObject {
synchronized public void method1(){…}
synchronized public void method2(){…}
}
public class myThread1 extends Thread {
private myObject object;
public myThread1 (myObjectobject){
this.object = object;
}
public void run(){
super.run();
object. method1 ();
}
}
public class myThread2 extends Thread {
private myObject object;
public myThread2 (myObjectobject){
this.object = object;
}
public void run(){
super.run();
object. method2();
}
}
public class myObject_main{
public static void main(String[] args) {
myObject object =new myObject ();
myThread1 a = newmyThread1(object);
myThread2 b = newmyThread2(object);
a.start();
b.start();
}
}
上面例子逻辑上是通过同一个对象去建立一种握手的联系
2. while轮询的方式:
这种方式主要实现逻辑是以条件为主要核心使线程间建立握手的联系,就是通过while使得线程不断的判断条件语句是否为true,然后通过另一个线程去改变这个条件语句的元素。这个方式十分的浪费资源,因为在另一个线程在不断的改变条件语句的元素时候,使用while轮询的那个线程在不断通过条件语句去检测条件,就相当于这个线程在这段时间只是不断的盯着,而不是释放CPU资源或者做其他更有意义的事情,所以造成了CPU资源和性能的浪费。
public class myThread1 extends Thread {
private List
list = new ArrayList (); public myThread1(Listlist) {
this.list = list;
}
public void run(){
for (int i = 0; i< 10; i++) {
list.add(","+i);
Thread.sleep(1000);
}
}
}
public class myThread2 extends Thread {
privateList
list = new ArrayList (); private Booleanisend = false;
public myThread2(List list) {
this.list = list;
}
public void run(){
while (!isend) {
if (list.size()==6) {
System.out.println("list的长度为6");
isend = true;
}
}
}
}
public class myWhile_main {
public staticvoid main(String[] args) {
List
mylist = new ArrayList (); myThread1 a = newmyThread1(mylist);
myThread2 b = newmyThread2(mylist);
a.start();
b.start();
}
}
3. wait/notify机制以及Lock/condition
wait/notify机制主要由以下三个方法来实现:wait()、notify() 和 notifyAll()
这三个方法都是在Object类中,并非在Thread类中,主要通过线程进入等待和由其他线程唤醒这个方式来建立线程之间的联系,实现线程之间的握手。使用这些方法的时候一定要定义在锁里面来产生互斥。
A.wait( )方法
让调用他的对象所在线程进入等待(阻塞)状态,并且让出锁(这一点区别于sleep,如果调用sleep地方是被锁住的,就不会释放锁)。wait()方法有三个重载方法:
wait()
wait(long millis)
wait(long millis, int nanos)
说明: millis为毫秒单位时间,nanos为纳秒单位时间,wait(long millis)是等待了millis毫秒之后恢复(没有进行通知的情况下),wait(long millis,int nanos)是等待millis毫秒+ nanos纳秒之后恢复(没有进行通知的情况下),默认的话只能等待通知。
B.notify( )方法
对象调用这个方法作用是通知某个处于等待状态(仅指之前调用了wait()方法而进入的阻塞状态) 的具有这一个对象控制权的线程现在继续运行。
C. notifyAll ( )方法
作用和notify( )方法一样,唯一的区别是notify( )方法的作用域是这个状态下的一个线程,而notifyAll ( )方法的作用域是这个状态下的所有线程。
例程:
public class myThread1 extends Thread {
private Object myobject;
public myThread1 (Objectmyobject) {
this. myobject = myobject;
}
public void run(){
synchronized (myobject){
for (int i = 0; i< 10; i++) {
System.out.println("你拍" + (i + 1));
if(i != 0){ myobject.notify();}
myobject.wait();
}
}
}
}
public class myThread2 extends Thread {
private Object myobject;
public myThread2(Objectmyobject) {
this. myobject = myobject;
}
public void run(){
synchronized (myobject){
for (int i = 0; i< 10; i++) {
System.out.println("我拍" + (i + 1));
myobject.notify();
if(i == 9){
System.out.println("结束");
}else{
myobject.wait();
}
}
}
}
}
public class waitnotifymain{
public staticvoid main(String[] args) {
Object myobject=new Object();
myThread1 a = newmyThread1(myobject);
a.start();
Thread.sleep(50);
myThread2 b = newmyThread2 (myobject);
b.start();
}
}
输出结果为:
你拍1
我拍1
你拍2
我拍2
…
你拍10
我拍10
结束
在main方法中,可以看到在a.start()和b.start()之间加入了Thread.sleep(50),使主线程暂停一段时间,当a和b都调用了start()后,a还未运行myobject.wait();时,b已经运行完了myobject.notify();,接下来a和b都运行了myobject.wait();并且一起等待notify()来通知,这样就线程就永久在这里停顿下去了,
Lock/Condition
从wait/notify的实现中不妨看出,运用wait/notify是必须加锁,也就是使用了synchronized关键字,在上文介绍同步时,加锁除了synchronized关键字外还有使用Lock-unLock。
如果要使用使用Lock加锁同时需要实现类wait/notify机制那样的协作(通讯)方式,那么就需要Condition接口了,主要是通过Condition接口里的三个方法:await()、signal()、signalAll()。这三个方法分别与wait()、notify()、notifyAll()功能上相对应,生成一个Condition对象一般是通过lock对象去调用newCondition()方法。我们可以用Lock/Condition接口来试着改写上面的例程:
public class myThread1 extends Thread {
pricate Locklock;
pricate Conditioncondition;
public myThread1 (Locklock, Condition condition) {
this. lock = lock;
this. condition = condition;
}
public void run(){
for (int i = 0; i< 10; i++) {
lock.lock();
try{
System.out.println("你拍" + (i + 1));
if(i != 0){ condition.signal();}//这里也可以用signalAll()
condition.await();
}finally{
lock.unlock();
}
}
}
}
public class myThread2 extends Thread {
pricate Locklock;
pricate Conditioncondition;
public myThread2(Locklock, Condition condition) {
this. lock = lock;
this. condition = condition;
}
public void run(){
for (int i = 0; i <10; i++) {
lock.lock();
try{
System.out.println("我拍" + (i + 1));
condition.signal();//这里也可以用signalAll()
if(i == 9){
System.out.println("结束");
}else{
condition.await();
}
}finally{
lock.unlock();
}
}
}
}
public class lockmain{
public staticvoid main(String[] args) {
pricate Lock lock = new ReentrantLock();
pricate Condition condition = lock. newCondition();
myThread1 a = newmyThread1(lock, condition);
a.start();
Thread.sleep(50);
myThread2 b = newmyThread2 (lock, condition);
b.start();
}
}
虽然使用condition接口的signal()、signalAll()对比使用notify()、notifyAll()更加的安全,但是通过上面的简单例程可以看出,condition接口来解决比通过wait/notify来解决更加的复杂。在实现上面例程这种简单的功能上,这种复杂性并不能换来多大的好处(或者说'带不来更多的收获'、'弊大于利'),所以在两者的选择上,要根据要解决的问题而定,或者说Lock/condition接口多数运用在更加困难复杂的多线程问题上。
4. 管道通信
任务之间通过输入/输出在线程间进行通信非常有用,而这种输入/输出的形式就类似于“管道”的形式,这里不得不提到实现其管道通信两两对应的四个类:
字字节流:PipedInputStream类 与 PipedOutputStream类
字符流:PipedReader类和 PipedWriter类
(这几个类在I/O流一文中已经介绍。)
以字符流为例:一个PipedReader类实例对象必须和一个PipedWriter类实例对象进行连接从而实现一个通信管道,允许任务通过PipedWriter对象向管道输出(写入)数据,允许不同的任务通过PipedReader对象(同一个管道)读取PipedWriter对象向管道中输出(写入)的数据,从而完成线程之间的通信。
class Sender extends Thread {
private PipedWriterout = new PipedWriter( );
public PipedWritergetPipedWriter(){return out;}
public BooleanIsend = flase;
private char c;
public void run(){
try{
while(!Isend){
for(c = 'A';c<'z';c++){
out.write(c);
Thread.sleep(500);
}
if(c == " z"){
Isend = true;
}
}
}catch(IOExceptione){
e.printStackTrace();
}
}
}
class Receiver extends Thread {
private PipedReaderin;
public BooleanIsend = flase;
publicReceiver(Sender sender){
in = new PipedReader(sender.getPipedWriter());
}
public void run(){
try{
while(!Isend){
System.out.print("Read:"+ (char)in.read() + ",")
if((char)in.read() == " z "){
Isend = true;
}
}
}catch(IOExceptione){
e.printStackTrace();
}
}
}
public class PipeText{
public staticvoid main(String[] args) throws Exception{
Sender sender =new Sender();
Receiver receiver= new Receiver(sender);
sender.start();
receiver.start();
}
}
输出:
Read:A, Read:B, Read:C, Read:D,….
以上介绍了五种较为基础的协作实现方式,当然还有很多,如BlockingQueue实现、使用join()、AtomicInteger等,可以单独了解,不再作介绍。
死锁
我们都知道,任务可以变成阻塞状态,那么这样的话,是否会出现一种情况,一个任务在等待另外一个任务,而另外一个任务又在等待别的任务,如此循环下去,直到最后一个任务又在等待第一个任务,形成一个互相等待闭环,或者更直接两个任务互相等待对方通知。
如果还记得上文wait/notify机制中提到的情形,那很明显的,上面假设的情况是有可能出现,在wait/notify机制中提到的情形就是最好的例子,两个线程都调用了wait()进入了等待状态等待被通知。这种最终任务形成互相等待的情况,就称为死锁。
死锁是一种潜在的缺陷,而且没有任何征兆,而且Java对死锁并没有提供语言层面上支持,所以如何避免死锁,只能取决于程序员精密的程序设计。
当然死锁也是有条件,在《Thinking in Java》一书有很好的总结出来,这里直接对其简单复述:
1、互斥条件。
2、至少有一个任务它必须持有一个资源,而且同时正在等待获取别的任务持有的资源。
3、不能抢占资源。
4、必须有循环等待(如while)
线程安全
线程是否安全,笼统(不全面)的概括就是一句话:所写成程序在单线程下运行和多线程下运行得到的结果是完全(绝对)一样,那么就是安全的。也就是说,线程安全标准就是线程是有加锁机制的,是提供数据访问保护,且不会出现数据和预期的不一致(数据污染、脏数据)。
如果对程序操作过程基础十分的熟练,程序逻辑足够缜密,能保证某一线程在做某些事情的时候,CPU资源不会被抢占,线程就是安全的。
线程安全问题基本都是由多个线程都可见的变量及静态变量引起的,因为这些变量并不受权限保护,也就是说每个线程都有对其修改的权利,当有线程涉及到对其进行写操作就要十分的注意是否存在线程安全问题。
再者,我们要考虑操作是否是原子性操作,原子性是操作时不可再分的操作,像i++就不是原子性操作,i++实际分为了三个步骤:从内存取到寄存器、寄存器自增、写回内存(简单总结就是:取值、加1、写回),如果非原子性操作(或者必须连续操作)的话,在操作中间就被抢占了CPU资源,那么就可能得到了脏数据。所以在这些操作的时候要保证CPU资源被其独占。
所以,保证线程安全,常常会运用到锁机制去"锁住"这些线程,保证运行,但是大量的使用锁机制会导致性能降低。
------------------------------------------------分割线----------------------------------------------------------
上文如有错误、写得不合理以及有疑惑的地方,希望您可以在评论区提出。
在下十分感谢。
如果只是想在评论区吐吐槽、聊聊心得、卖个萌,同样也十分欢迎。
祝大家生活美好,干杯!~( ゜▽゜)つロ
转载请注明原作者以及附上原文章地址,谢谢。