java之多线程
一、Thread
提到java中的线程,首先想到的是Runnable接口和Thread类。
Runnable很简单,只有一个run方法,通常需要通过Thread代理实现创建新线程。
new Thread(new Runnable() { @Override public void run() { System.out.println("hello"); } }).start();
Thread是Runnable的实现类,它拥有许多功能,下面简单介绍一下Thread的一些方法:
1、setPriority & getPriority
优先级在一定程度上可以控制线程获取cpu时间片的几率,不过因为cpu处理顺序的不确定性,不同优先级线程获得时间片的几率通常只有不大的差别,这也跟具体的平台有关。
setPriority和getPriority分别可设置和获取优先级,jdk中有10个优先级,但不同的操作系统中的优先级可能不同,所以我们最好使用MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY三个级别,主线程通常就是NORM_PRIORITY级别。
我们可以在任意时刻设置现成的优先级,只要线程没有结束,都是有效的。如果是在Runnable实现类中,需要在run方法中获取当前线程进行设置。
2、yield
yield方法只是对cpu调度的一种建议,它表明当前线程对于cpu的需求不是那么急迫,可以让给其它线程。不过,最终得到cpu资源的可能仍然是当前线程。
所以对于重要的控制和调整,不能依赖于此方法。
3、join
join方法需要在start方法后调用才生效,此方法将会造成所在线程阻塞,直到调用join的线程执行完毕才继续下去,所在线程可以是主线程也可以是工作线程。
join还有两个有参数的重载方法,参数为时间。所设时间为最长等待时间,如果时间到了,即使线程没执行完,所在线程也会不再阻塞,会继续执行下去。
public static void main(String[] args) { Thread t1 = new Thread("t1") { public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()); try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread t2 = new Thread("t2") { public void run() { try { t1.join(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName()); } }; t1.start(); t2.start(); }
输出:
t1
t1
t1
t1
t1
t2
t1
t1
t1
t1
t1
4、interrupt & isInterrupted & interrupted
isInterrupted和interrupted都是用于判断线程是否中断,其实通常就是判断是否调用了interrupt 方法。
isInterrupted和interrupted调用了同一个方法,只不过参数不同。isInterrupted只是查询状态,interrupted除了查询状态,还会清除状态,使状态变为false
例:
public static void main(String[] args) { Thread t = new Thread() { public void run() { interrupt(); System.out.println(isInterrupted()); System.out.println(isInterrupted()); System.out.println(interrupted()); //注意此处为interrupted System.out.println(isInterrupted()); } }; t.start(); }
输出:
true true true false
interrupt方法并不能使线程中断,它的主要功能是解除如sleep、join、wait等方法所造成的阻塞,阻塞解除时,这些方法都会抛出异常。
当interrupt解除阻塞时,中断状态为true,解除阻塞抛出异常后,中断状态变为false。
public static void main(String[] args) throws InterruptedException { Thread t = new Thread() { public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // e.printStackTrace(); } } }; t.start(); t.interrupt(); System.out.println(t.isInterrupted()); //输出:true(因为异步,不能100%确保此处在t抛出异常前执行) TimeUnit.MILLISECONDS.sleep(1); System.out.println(t.isInterrupted()); //输出:false }
5、setDaemon
此方法可将线程设置为后台线程,此方法需在start方法前调用。
后台线程特性:当其它非后台线程结束时,后台线程将随着jvm的终止而结束;后台线程中创建的线程也是后台线程。
Callable:
相比于Runnable,Callable多出了返回值。
下面为Callable的简单使用示例:
public static void main(String[] args) { ExecutorService service=Executors.newCachedThreadPool(); Callable<String> command=new Callable<String>() { @Override public String call() throws Exception { TimeUnit.SECONDS.sleep(2); return "call result"; } }; Future<String> future=service.submit(command); while(!future.isDone()){ } try { System.out.println(future.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } service.shutdown(); /* output: call result */ }
Callable需要ExecutorService对象调用submit方法来启动,submit方法会返回一个Future对象。
Future对象的isDone方法判断Callable线程是否执行完毕;get方法将获得call方法中的返回值,不过此方法有可能会造成阻塞,会等到Callable执行完毕才会返回。
我们可以在调用get方法前先通过isDone方法判断线程是否执行完毕,或者使用可设置超时时间的有参get方法。
ExecutorService:
ExecutorService用于管理线程,简化并发开发。常见的ExecutorService有CachedThreadPool、FixedThreadPoll、SingleThreadPool、ScheduledThreadPoolExecutor。
1.CachedThreadPool
CachedThreadPool会根据需求创建线程,添加新任务后,如果有空闲线程,新任务会放到空闲线程中运行,否则重新创建新线程。
当一个线程结束,它会继续保留1分钟时间,在一分钟内如果有新任务,新任务将会分配到已经结束的空闲线程中,如果没有新任务,一分钟后空闲线程将会关闭。
在CachedThreadPool和FixedThreadPool中,前者更加灵活,通常作为优先选项。
2.FixedThreadPoll
FixedThreadPoll可以设置最大线程数,且若不调用shutdown或shutdownNow方法,创建的线程不会关闭。
3.SingleThreadPool
可以看作是线程数为1的FixThreadPool。
4、ScheduledThreadPoolExecutor
这里需要提一下的是scheduleWithFixedDelay和scheduleAtFixedRate方法的区别:
两者只是在延迟方面有些差异,前者是在上一次任务完成后延迟所设时间再进行下一次;后者是在上一次开始后延迟所设时间在进行下一次,不过如果此时上一次未完成,要等待完成再进行下一次。
ExecutorService的常用方法:
1.shutdown&shutdownNow
shutdown方法调用后,ExecutorService不能再添加新的任务;shutdownNow方法调用后也不能在添加新的任务,同时,解除正在运行线程的阻塞状态,并返回未运行的任务的Collection集合。
2.isShutdown&isTerminated&awaitTermination
isShutdown用于判断ExecutorService是否关闭,其实就是看有没有调用shutdown或shutdownNow方法。
isTerminated则是判断任务是否终止,任务终止才为true,而任务终止的条件是:
1).调用showdown或showdownNow方法。
2).任务完成ExecutorService不再工作。
awaitTermination与isTerminated类似,不过相比于isTerminated增加了超时时间。此方法会造成阻塞,无论是线程池任务完成还是超时时间到了都返回值。
3.invokeAll&invokeAny
前者在所有任务完成后返回值,后者在有任何一个任务完成就返回值,两者都会阻塞线程,两者分别都有一个超时重载方法。
volatile:
根据JMM原理,在多线程工作中,共享在资源保存在主内存中,但是工作线程不能直接对主存中的数据进行造作,而是在各自的线程中创建一个缓存副本,对副本中的数据进行操作。
volatile修饰的数据具有可见性,各个线程在读取这类数据时都会从主存中读取,每次写入也都会刷新到主存。
但是volatile并不具有原子性,在使用它时,通常要遵循两个条件:
1、对变量的操作不能依赖于它之前的值
2、变量的值不能受其它变量的限制
例如当我们在循环模式中使用的boolean类型的标记就通常使用volatile修饰。
受限于这两个个条件,volatile的适用场合非常小,再加上容易出错,所以我们的首选方法通常是使用synchronized。
synchronized:
synchronized具有原子性,在实行Synchronized保护的代码时,要经过一下几个步骤:
检查锁是否可用——获取锁——执行代码——释放锁。
synchronized可分为方法锁和同步块,两者又分别可分为静态和非静态。
方法锁:
方法锁主要有以下几个特点:
1.在一个类中,所有静态方法的锁是同一把锁——类锁,因此它们是互斥的;
2.在一个对象中,所有的非静态方法也是共用一把锁——对象锁,它们之间也互斥;
3.同一个类的不同对象,非静态方法用的锁是不同的。
同步块:
同步块中的锁也可分为类锁与方法锁,稍有不同的是,在一个类中,可以有多个锁,最后究竟是否互斥,只需判断锁是否是同一个对象即可。
通常,我们会将锁对象设置为private,以防止不小心改变锁,而产生错误,下面就是一个错误的示例,它会抛出空指针异常:
public class Test { public static void main(String[] args) throws InterruptedException { Worker w = new Worker(); w.start(); TimeUnit.SECONDS.sleep(3); w.object = null; } } class Worker extends Thread { Object object = new Object(); public void test() { synchronized (object) { System.out.println("hello"); } } @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test(); } } }
除了使用synchronized关键字,也可以显示调用Lock对象来达到目的:
public class Test { public static void main(String[] args) { MyRunnable task=new MyRunnable(); for (int i = 0; i < 1000; i++) { new Thread(task).start(); } while(Thread.activeCount()>1){ Thread.yield(); } System.out.println(task.getCount()); } } class MyRunnable implements Runnable { private Lock lock = new ReentrantLock(); private int count = 0; public int getCount() { return count; } @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { count++; } finally { lock.unlock(); } } }
这种方法使用了try-finnally,看起来代码比synchronized多,但是它有个有点,在出现异常时,可以在finnally中进行清理工作,使系统保持良好的状态。
需要注意的是,如果有返回值,需要在try中return,防止unlock过早调用,破坏了原子性。
大体上,使用synchronized更加简单,出错的可能性更小,所以,如果没有特殊情况,通常优先使用synchronized。
不过Lock对象可以实现一些synchronized无法实现的一些功能,如Lock对象可以尝试获取锁,然后根据是否获取成功进行处理:
private Lock lock = new ReentrantLock(); public void test() { if (lock.tryLock()) { try { //获取锁后的操作 } finally { lock.unlock(); } } else { //未获取锁的操作 } }
原子类:
jdk中包含了诸如AtomicInteger、AtomicReference、AtomicBoolean、AtomicLong等原子性变量类,通过他们可以实现一些原子操作:
public class Test { public static void main(String[] args) { MyRunnable task = new MyRunnable(); for (int i = 0; i < 10000; i++) { new Thread(task).start(); } while(Thread.activeCount()>1){ Thread.yield(); } System.out.println(task.getCount()); } } class MyRunnable implements Runnable { private AtomicInteger count=new AtomicInteger(0); public int getCount(){ return count.get(); } @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count.addAndGet(1); } }
wait、notify、notifyAll:
wait方法能使能使线程挂起,等到被notify或notifyAll唤醒才继续执行下去,这个功能有点类似于空循环,直到某个条件达成才中断循环,不过空循环会浪费cpu资源,是一种不良的使用方式。
wait此方法能够释放锁,在wait等待期间与它原本互斥的synchronized同步块可以执行。
wait还有一个超时方法,当超时后,线程自动唤醒,继续执行下去,当然,前提是你得获得锁。
notify只能唤醒一个wait中的线程,而notifyAll能唤醒所有wait中的线程,当然,这得在同一个锁的基础上。所以当我们看到wait无法被唤醒使,去看看是否是他们是否是同一个锁,主要是在以this为锁的时候容易犯错。
CountDownLatch:
我们可能会遇到这样的情况,有个任务task需要等待task0,task1……taskn都完成后才能执行,要实现这个效果,可以使用join方法,不过使用CountDownLatch更加简单:
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class T { public static void main(String[] args) { final int COUNT=10; CountDownLatch latch=new CountDownLatch(COUNT); ExecutorService service=Executors.newCachedThreadPool(); for (int i = 0; i < COUNT; i++) { service.execute(new MyRunnable(latch)); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("all tasks are finished"); service.shutdown(); } } class MyRunnable implements Runnable{ private final CountDownLatch latch; public MyRunnable(CountDownLatch latch) { this.latch=latch; } @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count"); latch.countDown(); } }
Semaphore
semaphore是计数信号量,可通过acquire请求获得信号量,release方法释放信号量。
线程运行时请求获得信号量,如果semaphore中的资源不足,则会造成阻塞,等获得信号量时才继续运行下去。
例子:比如公共厕所有两个蹲位,同一时间最多只能有两个人使用。
public class Test { public static void main(String[] args) { for(int i =0 ;i< 10;i++) { new Worker("hero:"+i).start();; } } } class Worker extends Thread{ static Semaphore semaphore =new Semaphore(2); public Worker(String name) { super(name); } @Override public void run() { try { semaphore.acquire(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName()+" 获得宝座"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName()+" 退位让贤"); semaphore.release(); } }