接口Runnable
和类Thread
的区别
Runnable -> run()
: 不是有单独的线程驱动的,需要依托其他线程Thread -> run()
: 具有自己的线程
使用Executor
进行线程管理
- 不占用当前启动线程的资源
- 程序会在调用
shutdown()
之前提交的所有任务完成后结束 ExecutorService
:CachedThreadPool
: 为每一个任务创建一个线程(一般用这个就好)FixedThreadPool
: 可以控制线程的数量SingleThreadPool
: 仅有一个线程,类似线程数量为1的FixedThreadPool
- 范例:
class LiftOff implements Runnable { private static int taskCount = 0; private final int id = taskCount++; @Override public void run() { for(int i=0; i<10; i++) System.out.println("["+id+"]("+Thread.currentThread().getId()+") " + i); } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); // ExecutorService exec = Executors.newFixedThreadPool(3); // 使用有限的线程集完成并发 for(int i=0; i<5; i++) exec.execute(new LiftOff()); // 防止新任务被提交给这个Executor exec.shutdown(); } }
线程的一些基本操作
- 休眠:
Thread.sleep(milliseconds)
: 休眠一段时间,参数为毫秒 - 优先级
- 查看优先级:
Thread.currentThread().getPriority()
- 设置优先级:
Thread.currentThread().setPriority()
- 参数
Thread.MAX_PRIORITY
最高(10)Thread.NORM_PRIORITY
中等(5)Thread.MIN_PRIORITY
最低(1)
- 注意: 尽管JDK有10个优先级,但它与多数操作系统都不能映射得很好。比如Windows7有7个优先级且不固定,所以这种映射关系也不是确定的。唯一可移植的方法是当调整优先级别的时候,只使用
MAX_PRIORITY
、NORM_PRIORITY
和MIN_PRIORITY
三种级别。
- 查看优先级:
- 让步: 使用
Thread.yeild()
进行暗示,申请可以将资源调度给其他线程使用,但系统未必会切换线程 - 后台线程
- 程序在运行时在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分
- 当所有的非后台线程结束后,程序也就终止了,同时会杀死所有后台进程
- 将线程设置为后台线程需要在线程启动前设置
thread.setDaemo(true)
- 加入一个线程: 如果在某个线程
x
中让线程t
上调用t.join()
,线程x
将被挂起,直到线程t
结束才恢复(即it.isAlive()
返回为false)。对t.join()
方法的调用可以被中断,做法为调用x.interrupt()
。如果线程x
被中断或正常结束,线程t
也将和x
一同结束 在线程中捕获异常,默认会向外传播到外层
- 使用
Exector
来管理线程的创建,在每个创建时附上一个异常处理器Thread.UncaughtExceptionHandler
/** 测试方法 */ public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(new CacheExceptionThreadFactory()); exec.execute(new Thread() { @Override public void run() { System.out.println("R线程名: " + getId()); System.out.println("R: " + currentThread().getUncaughtExceptionHandler().toString()); throw new RuntimeException("给你个异常瞧瞧"); } }); exec.shutdown(); } /** 异常处理器 */ class myUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { System.out.println("E线程名: " + t.getId() + "异常: " + e.getMessage()); } @Override public String toString() { return "哈哈"+super.toString(); } } /** 线程生成工厂 */ class CacheExceptionThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setUncaughtExceptionHandler(new myUncaughtExceptionHandler()); return t; } }
- 使用
资源共享竞争
volatile
: 修饰属性。保证数据在被修改后立即能写回内存,使得其他线程能读取到修改后的数据synchronized
: 修饰方法或语句块、。在上一个调用方法结束之前,其他调用该方法的线程全都被阻塞。使用
Lock
实现互斥机制,相比于synchronized
的简洁性,显式使用Lock
可以通过finally
将系统维护在正确的状态,而不像synchronized
出现错误后仅仅抛出一个异常。private Lock lock = new ReentrantLock(); public void readOrWrite() { lock.lock(); try{ // some operators // return 必须出现在try{}中,确保unlock()不会过早发生,将数据暴露给下一个任务 return; } finally { lock.unlock(); } }
tryLock(long timeout, TimeUnit unit)
可以设置获取锁的时间,如果在设定的时间内无法获取锁,可以先进行其他操作。- 原子性: 对基本数据类型的读取和赋值操作被认为是安全的原子性操作。
- 原子类:
AtomicInteger
AtomicLong
AtomicReference
等
同步控制块:
synchronized(syncObject) { // This code can be access by only task at a time }
自增线程安全性测试及解决方案
- 原因: 多个线程同时访问共享变量
i
,而JVM允许每个线程存储变量的副本,i++
的操作可以分为三步: 取值、自增、写回。存在一个线程在 自增 时,刚好有线程在 取值,因此最后会出现i
增加的结果总比预计的结果线程小。 - 测试例:
class TestIPlus {
private int val = 0;
public void run() {
for(int i=0; i<10; i++) {
this.val = 0;
final CountDownLatch count = new CountDownLatch(10000);
for(int j=0; j<100; j++) {
new Thread(){
@Override
public void run() {
for(int i=0; i<100; i++) {
TestIPlus.this.val++;
count.countDown();
}
}
}.start();
}
try {
count.await();
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.val);
}
}
}
使
i++
变得线程安全有3种方式:- 使用
synchronized
关键字,将i++
写成一个方法,并使用synchronized
修饰
public synchronized void incI() { this.i++; }
- 使用
Lock
,在修改i
的位置加锁
private Lock lock = new ReentrantLock(); public void incI() { lock.lock(); try { i++; } finally { lock.unlock(); } }
- 使用原子类
AtomicInteger
class TestIPlus { private AtomicInteger val; public void run() { for(int i=0; i<10; i++) { this.val = new AtomicInteger(0); final CountDownLatch count = new CountDownLatch(10000); for(int j=0; j<100; j++) { new Thread() { @Override public void run() { for(int i=0; i<100; i++) { // 原子类自增 TestIPlus.this.val.getAndIncrement(); count.countDown(); } } }.start(); } try { count.await(); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println(this.val); } } }
- 使用
线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。创建和管理线程本地存储可以由java.lang.ThreadLocal
类来实现。ThreadLocal
对象通常当做静态域存储,在创建时,只能通过get()
和set()
方法来访问该对象的内容。
class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> val = new ThreadLocal<Integer>() {
private Random rand = new Random(47);
protected Integer initialValue() {
return rand.nextInt(1000);
}
};
public static void increment() {
val.set(val.get() + 1);
}
public static Integer getValue() {
return val.get();
}
}
class Accessor implements Runnable {
private final int id;
public Accessor(int id) {
this.id = id;
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
// 每个线程都有自己的val
System.out.println(this);
}
}
@Override
public String toString() {
return "#" + id + ": " + ThreadLocalVariableHolder.getValue();
}
}
线程四种状态
- 新建(New):当线程被创建时,它只会短暂的处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。
- 就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配到时间片给线程,它就可以运行。
- 阻塞(Blocked):线程能够运行,但有某个条件阻止了它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入就绪状态,它才有可能执行操作。
- 死亡(Dead):处于死亡或终止状态的线程将不会再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从
run()
方法返回,但是任务的线程还可以被中断。
线程出现阻塞的原因
- 调用
sleep(millseconds)
是任务进入休眠。 - 调用
wait()
使线程挂起。直到线程得到notify()
或notifyAll()
消息(或者在JavaSE5的java.util.concurrent
类库中等价的signal()
或signalAll()
消息)使线程进入就绪状态。 - 任务在等待某个输入/输出完成。
- 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获得了这个锁。