- Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每一个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。
- 线程使你能够创建更加松散耦合的设计。无需显式地关注通常可以由线程来处理的任务。
- 通过使用多线程机制,这些独立任务 (也被称为子任务)中的每一个都将由执行线程来驱动。
- 定义任务:只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。但它不会产生任何内在的线程能力,要实现线程行为,你必须显式地将一个任务附着在线程上。
class RunnableImpl implements Runnable {
public RunnableImpl() {
System.out.println(Thread.currentThread().getName()+" constructed!");
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
Thread.yield();
}
}
}
- Thread类:将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。实际上,Thread内部有一个Runnable对象,但如果构造器中传入Runnable对象之后,会替换成该Runnable对象。构造器中还可以加入
String
类型的参数,用来设置该线程的名称。public class test{
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new RunnableImpl()).start();
}
}
}
调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run方法,以便在这个新线程中启动该任务。
每个Thread都注册了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。因此,一个线程会创建一个单独的执行线程,在对start()的调用完成之后,它仍旧会继续存在。
- 使用Executor:Executor(执行器)将会为你管理Thread对象,从而简化了并发编程。
ExecutorService
是具有服务生命周期的Executor
,知道如何构建恰当的上下文来执行Runnable对象。
shutdown()
方法的调用可以防止新任务被提交到这个Executor,当前线程将会继续运行在shutdown()
方法调用之前提交的所有任务。ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new RunnableTask());
}
exec.shutdown();
有了FixedTreadPool
可以一次性预先执行代价高昂的线程分配,因此也就可以限制线程的数量了。这可以节省时间,因为不用为每个任务都固定地赋初创建线程的开销。
当线程用完之后,接下来的任务会等待其他任务结束之后,再开始执行。ExecutorService exec = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
exec.execute(new RunnableTask());
}
exec.shutdown();
SingleThreadExecutor
就像是线程数量为1的FixedTreadPool
。对于你希望在另一个线程中连续运行的任何事物来说,都是很有用的。比如,监听进入的套接字连接任务,更新本地或远程日志的小任务,或者是事件分发线程。ExecutorService exec = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
exec.execute(new RunnableTask());
}
exec.shutdown();
可以用SingleThreadExecutor
来执行多个线程对同一个文件系统的操作,实现任意时刻都只有唯一的任务在运行。
- 在任务中产生返回值
实现Callable接口,通过ExecutorService.submit()
方法调用它。
submit()
会产生Future对象,它用Callable
返回结果的特定类型进行了参数化。可以用isDone()
方法来查询Future是否已经完成,如果任务完成时,它会具有一个结果,可以调用get()
方法来获取该结果。也可以直接调用get()
,如果还没完成,get()
就会阻塞,直至结果准备就绪。
public class CallableImpl implements Callable<String> {
private static int count = 0;
private final int id;
public CallableImpl() {
this.id = count++;
}
@Override
public String call() throws Exception {
return "id: " + id;
}
}
public class CallableTest {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> strings = new ArrayList<>();
for (int i = 0; i < 5; i++) {
strings.add(exec.submit(new CallableTask()));
}
for (Future<String> string : strings) {
try {
System.out.println(string.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
exec.shutdown();
}
}
}
}
- 休眠
任务中止执行给定的时间。Thread.sleep(100);
TimeUnit.MILLISECONDS.sleep(100);
- 优先级
线程的优先级将该线程的重要性传递给调度器。调度器将倾向于让优先权最高的线程先执行,但不意味着优先权低的线程就不执行。但是,在绝大多数情况下,所有线程都应该以默认优先级执行。
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
- 让步:给线程调度机制一个暗示,工作已经完成地差不多了,可以给其他线程使用CPU了。但是,这是建议由相同优先级的其他线程可以运行。
Thread.yield();
- 后台线程:是指程序运行的时候,在后台提供的一种通用服务的线程,并且这种线程并不属于程序不可或缺的部分。因此,在所有非后台线程结束时,程序就终止了,所有后台进程也会被杀死。
public class DaemonTest {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread daemon = new Thread(new SimpleDaemon());
daemon.setDaemon(true);
daemon.start();
}
System.out.println("All daemon has built!");
TimeUnit.MILLISECONDS.sleep(1000);
}
}
class SimpleDaemon implements Runnable {
@Override
public void run() {
try {
while (true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + " " + this);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
必须在start()
之前设置为后台线程。
可以通过编写定制的ThreadFactory
定制由Executor
创建的线程的属性(后台、优先级、名称)。class DaemonThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
}
应该是一个工厂模式,可以给出创建的对象之前,处理预处理一些内容。public class ThreadFactoryTest implements Runnable{
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());
for (int i = 0; i < 5; i++) {
exec.execute(new ThreadFactoryTest());
}
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while (true){
System.out.println(Thread.currentThread().getName());
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
后台线程创建的线程,也是后台线程。
- 编码的变体
也可以通过继承Thread
类,重写run()
方法来实现。class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
– 自管理的Runnable
有可能变得有问题,因为另一个任务可能在构造器结束之前开始执行,这意味着该任务能够访问处于不稳定状态的对象。class RunnableImpl implements Runnable {
private Thread t = new Thread(this);
public RunnableImpl() {
t.start();
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
Thread.yield();
}
}
}
– 线程代码隐藏在类中class InnerThread1 {
private Inner inner;
private class Inner extends Thread {
Inner(String name) {
super(name);
start();
}
@Override
public void run() {
...
}
}
public InnerThread1(String name) {
inner = new Inner(name);
}
}
class InnerThread2 {
private Thread t;
public InnerThread2(String name) {
t = new Thread(name) {
@Override
public void run() {
...
}
}
t.start();
}
}
class InnerRunnable1 {
private Inner inner;
public InnerRunnable1(String name) {
inner = new Inner(name);
}
private class Inner implements Runnable {
Thread t;
public Inner(String name) {
t = new Thread(this, name);
t.start();
}
@Override
public void run() {
...
}
}
}
class InnerRunnable2 {
private Thead t;
public InnerRunnable2(String name) {
t = new Thread(new Runnable() {
@Override
public void run() {
...
}
}, name);
t.start();
}
}
class ThreadMethod {
private Thread t;
private String name;
public ThreadMethod(String name) {
this.name = name;
}
public void runTask() {
t = new Thread(name) {
@Override
public void run() {
...
}
}
t.start();
}
}
- 加入一个线程
一个线程可以通过其他线程之上调用join()方法,效果是此线程会被挂起,直到目标线程结束,才恢复。class Joiner extends Thread {
Thread t;
public Joiner (Thread t) {
this.t = t;
}
@Override
public void run() {
...
t.join();
...
}
}
- 捕获异常
为了解决在run()
方法中,无法捕获异常的问题。允许在每个Thread中附着一个异常处理器Thread.UncaughtExceptionHandler.uncaughtException()
会在线程因未捕获的异常而临近死亡时被调用。为了使用它,需要创建一个新类型的ThreadFactory
,它将在每个新创建的Thread
对象上附着一个Thread.UncaughtExceptionHandler
。public class ExceptionHandlerTest {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(new MyThreadFactory());
exec.execute(new Runnable() {
@Override
public void run() {
throw new RuntimeException();
}
});
}
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " caught " + e);
}
}
class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
return t;
}
}
或者在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
共享受限资源
- 对于并发工作,你需要某些方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况。
- 基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常通过在代码前面加上一句锁语句来实现。锁语句产生了一种互相排斥的效果,所以这种机制常常被称为互斥量(mutex)。
- 所有要访问同一个共享资源的方法都要标志为
synchronized
。如果某个任务处于一个对标记为synchronized
的方法的调用中,那么在这个线程从这个方法返回之前,其他所有要调用类中任何标记为synchronized
的方法都会被阻塞。
- 所有对象都自动含有一把单一的锁。所有
synchronized
共享一把锁,这样才可以防止多个任务同时访问被编码的对象内存
- 在使用并发的时候,将域(属性)设置为private很重要。外面的对象只能通过方法调用来获取域。否则,外界直接获取并设置域就好了。
public synchronized int getXxx(){
}
public synchronized int setXxx(){
}
- Lock对象:显示地创建、锁定、释放。相比较内建的锁,缺乏优雅性,但具备灵活性。
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
...
lock.unlock();
}
public class AttemptLocking {
private ReentrantLock lock = new ReetrantLock();
private void untimed() {
boolean captured = lock.tryLock();
try{
System.out.println("tryLock(): " + captured);
} finally {
if (captured)
lock.unlocked();
}
}
}
- 原子性和易变性
– 原子性可以应用于除long
和double
之外的所有基本数据类型之上的“简单操作”。对于读取和写入除long
和double
之外的基本类型变量这样的操作,可以保证它们会被当做不可分(原子)的操作来操作内存
– 当定义long
和double
变量时,如果使用volatile
关键字,就会获得(简单的赋值与返回操作的)原子性。
– volatile
关键字还确保了应用中的可视性,如果你将一个域声明为volatile
的,那么只要你对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。即使使用了本地缓存,情况也确实如此,volatile
域会立即被写入到内存中,而读取操作就发生在主存中。
– 如果一个域完全由synchronized
方法或语句块来防护,那么就不必将其设置为volatile
的。
– 以下操作均不是原子性的。其中包括取数字、加法、存数字几个操作。i++;
i += 2;
- 原子类:
AtomicInteger
、AtomicLong
、AtomicReference
等特殊的原子性变量类。这些类被调整为可以使用在某些现代处理器上的可获得的,并且是在机器级别上的原子性。在涉及性能调优时,它们就大有用武之地。public class AtomicIntegerTest implements Runnable {
private AtomicInteger i = new AtomicInteger(0);
public int getValue() {
return i; }
private int evenIncrement() {
return i.addAndGet(2); }
public void run() {
while (true) {
evenIncrement();
}
}
}
- 临界区:为了防止多个线程同时方法内部的部分代码而非防止访问整个方法。通过这种方法分离出来的代码被称为临界区。它也可以使用
synchronized
关键字建立。这里,sychronized
被用来指定某个对象。synchronized (syncObject) {
}
- 线程本地存储:是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。
因此,如果有5个线程都要使用变量x所表示的对象,拿线程本地存储就会生成5个用于x的不同的存储块。创建和管理线程本地存储可以由java.lang.ThreadLocal
类来实现class Accessor implements Runnable {
private final int id;
public Accessor(int idn) {
id = idn; }
public void run() {
while(!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#" + id + ":" + ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<>() {
private Random rand = new Random(47);
@Override
protected synchronized Integer InitialValue() {
return rand.nextInt(10000);
}
}
public static void increment() {
value.set(value.get() + 1);
}
public static int get() {
return value.get(); }
public static void main(String[] args) {
ExecutorService exec = Exectors.newCachedThreadPool();
for(int i = 0; i < 5; i++) {
exec.execute(new Accessor(i));
}
TimeUnit.SECONDS.sleep(3);
exec.shutdownNow();
}
}
当运行这个程序时,可以看到每个单独的线程都被分配了自己的存储。因为它们每个都需要跟踪自己的计数值,即使只有一个TheadLocalVariableHolder
对象。