-一、线程和进程
进程:进程是运行在它自己的地址空间内的自包容的程序线程:线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。一个程序至少有一个进程,一个进程至少有一个线程. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
二、任务 每个任务都是Runnable接口的实例,为可运行对象,这些独立任务中的每一个都将由执行线程来驱动。
public class Test1 implements Runnable{ private int num = 0; private int endNum = 10; private int id; public Test1(int endNum, int id) { this.endNum = endNum; this.id = id; } @Override public void run() { while(++num <= endNum){ System.out.println("#id:" + id + "num:" + num); } } public static void main(String[] args) { for (int i = 0; i < 2; i++) { Thread thread = new Thread(new Test1(5, i)); thread.start(); } System.out.println("end"); } }输出:
end #id:0 num:1 #id:2 num:1 #id:2 num:2 #id:2 num:3 #id:1 num:1 #id:1 num:2 #id:1 num:3 #id:0 num:2 #id:0 num:3这说明main函数执行完毕之后,任务仍然在进行,因为 1、JVM会在所有的非守护线程(用户线程)执行完毕后退出; 2、main线程是用户线程; 3、仅有main线程一个用户线程执行完毕,不能决定JVM是否退出,也即是说main线程并不一定是最后一个退出的线程
每个Thread都“注册”了自己,它的任务退出其run()并死亡之前,垃圾回收器无法清除它,一个线程会创建一个单独的执行线程,在对start()的调用完成之后,它仍旧会继续存在
三、基本的线程机制
1、Executor Executor(执行器)在客户端和任务执行之间提供了一个间接层,这个中介对象将执行任务。 CachedThreadPool在程序执行过程中通常会常见与所需数量相同的线程,然后在它回收线程是停止创建新的线程。 FiexedThreadPool可以一次性预先执行代价高昂的线程分配。 SingleThreadExecutor,如果提交了多个任务,它们将排队执行。
2、优先级 CUP处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先级最高的线程先执行。
public class PriorityTest implements Runnable { private int priority; private int id; public PriorityTest(int priority, int id) { this.priority = priority; this.id = id; } @Override public void run() { Thread.currentThread().setPriority(priority);//Thread.currentThread()来获得对驱动该任务的Thread对象的引用 for (int i = 0; i < 4; i++) { System.out.println("#id:" + id + " num:" + i); } } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new PriorityTest(Thread.MIN_PRIORITY, 1));//最低 exec.execute(new PriorityTest(Thread.MAX_PRIORITY, 2));//最高 exec.isShutdown();//关闭执行器,但允许完成执行器中的任务,可以防止新的任务被提交给这个Executor } }输出:
#id:2 num:0 #id:2 num:1 #id:2 num:2 #id:2 num:3 #id:1 num:0 #id:1 num:1 #id:1 num:2 #id:1 num:3优先级的设定是在run()开头部分设定的,因为Executor在构造器的时候还没有开始执行任务。
3、利用Callable从任务中产生返回值 Callable是一种具有类型参数的泛型,他的类型参数表示的是从方法call()中返回的值。使用exec.submit()方法调用,该方法产生Future对象,对特定的类型进行了参数化。
class Task implements Callable<String>{ private int id; public Task(int id) { this.id = id; } @Override public String call() throws Exception { return "Task" + id; } } public class CallableTest { //带返回值的任务,使用Callable接口 public static void main(String[] args) { System.out.println("start"); ExecutorService exec = Executors.newCachedThreadPool(); ArrayList<Future<String>> results = new ArrayList<>(); //Future对返回结果的特定类型进行了参数化 for (int i = 0; i < 5; i++) { results.add(exec.submit(new Task(i))); //使用ExecutorService.sumit()方法调用 } for (Future<String> future : results) { try { System.out.println(future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } System.out.println("end"); } }输出:
start Task0 Task1 Task2 Task3 Task4 end4、后台线程 后台线程是指在线程允许的时候在后台提供一种通用服务的线程,并且在这种线程 并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止,同时会杀死进程中的所有后台线程(main()为非后台线程)
public class DaemonTest implements Runnable{ private int i = 0; @Override public void run() { try { while (true) { Thread.sleep(100); System.out.println(++i); } } catch (InterruptedException e) { e.printStackTrace(); }finally { System.out.println("deamon end"); } } public static void main(String[] args) { Thread thread = new Thread(new DaemonTest()); thread.setDaemon(true);//设置为后台线程 thread.start(); try { Thread.sleep(600); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); } }输出:
1 2 3 4 5 end在main()中设置了睡眠的时间,main()结束后,后台线程也终止,所以不再会有输出。并且,后台线程在不执行finally子句的情况下就会终止其run()方法。
5、捕获异常 Thread的run方法是不抛出任何检查型异常(checked exception)的,但是它自身却可能因为一个异常而被终止,导致这个线程的终结。在线程中抛出的异常即使使用try-catch也无法截获。 Thread.UnCaughtExceptionHandler可以在Thead对象中附着一个异常处理
public class UnCaughtExceptionTest implements Runnable{ //这是一个自管理的Runnable private Thread thread; public UnCaughtExceptionTest() { thread = new Thread(this); thread.setUncaughtExceptionHandler(new MyHandler());//设置异常处理器 thread.start(); } @Override public void run() { throw new RuntimeException();//抛出异常 } class MyHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) {//捕获异常 System.out.println("thread:" + toString() + "\n e:" + e); } } public static void main(String[] args) { new UnCaughtExceptionTest(); } }结果:
thread:com.example.threadtest.UnCaughtExceptionTest$MyHandler@663e0ba5 e:java.lang.RuntimeException6、方法
sleep()//使任务中止执行给定的时间 yield()//使线程暂停并允许执行其他线程 join()//等待线程结束 interrupt()//中断线程 isAlive()//测试当前程序是否在允许 setPriority(p:int)//设置线程的优先级四、共享受限资源 有了并发可以同时做很多事,但是也会产生一些问题
class NumGetter{ private int num; public int next(){ ++num;//递增不是原子性操作 /*递增的操作不是原子的,因为它不会作为一个不可分割的操作来执行。它实际包含了三个独立的操作,读取i的值,将值加1,然后将计算结果写入i。这是一个读取—修改—写入的操作序列*/ try { Thread.sleep(20);//使线程睡眠一段时间 } catch (InterruptedException e) { e.printStackTrace(); } return num; } } class Checker implements Runnable{ private NumGetter numGetter; public Checker(NumGetter numGetter) { this.numGetter = numGetter; } @Override public void run() { System.out.println(numGetter.next()); } public static void test(NumGetter numGetter){ ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { exec.execute(new Checker(numGetter)); } exec.shutdown(); } } public class CheckAdd { public static void main(String[] args) { Checker.test(new NumGetter()); } }看起来应该输出1,2,3,4,5,但是实际输出了:
5 5 5 5 5因为自加后,调用了Thread.sleep(),其他线程对数据进行了更改,最后返回了5
解决共享资源竞争 对于并发工作,需要某种方式来防止两个任务访问相同的资源 1、synchronized关键字 关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。并且保证进入同步方法或同步代码块的每一个线程,都看到由同一个锁保护的之前所有的修改效果。
如果一个类中有超过一个方法在使用临界数据,那么必须同步所有相关的方法,如果只是同步一个方法,其他的方法可能会忽略这个锁。每个访问临界共享数据的方法都必须被同步
将上面的例子改为
public synchronized int next(){ //... }结果:
1 2 3 4 5可以阻止其他线程访问,第一个进入next()的任务获得锁,任何其他视图获取锁的任务都将从其开始尝试之前被阻塞,直至第一个任务释放锁。
同步块 同步语句允许设置同步方法中的部分代码,而不必是整个方法,可以使多个任务访问对象的时间性能得到显著的提高。使在安全的情况下其他线程更多的访问
synchronized (this) { //... }另外一种,在另一个对象上同步,下面是两个任务可以同时进入同一个对象,只要这个对象上的方法是在不同的锁上同步的:
public class SynchronizedTest1 { private static Object syncObject = new Object(); public static synchronized void a(){ for (int i = 0; i < 5; i++) { System.out.println("a()"); Thread.yield(); } } public static void b(){ synchronized (syncObject) { for (int i = 0; i < 5; i++) { System.out.println("b()"); Thread.yield(); } } } public static void main(String[] args) { SynchronizedTest1 test1 = new SynchronizedTest1(); new Thread(new Runnable() { @Override public void run() { test1.a(); } }).start(); test1.b(); } }结果:
b() a() b() a() b() a() b() a() b() a()a()在this同步,b()有一个syncObject上的同步synchronized块,两个同步是相互独立的。两个方式在同时运行,因此任何一个方法都没有因为对另一个方法的同步而阻塞。 如果换成:
synchronized (SynchronizedTest1.class){ }则这两个方法不允许同时被线程访问。
对于实例同步方法,锁是当前实例对象。(this) 对于静态同步方法,锁是当前对象的Class对象。(o.class) 对于同步方法块,锁是Synchonized括号里配置的对象。一篇便于理解的文章:http://pengfei.iteye.com/blog/1358833
一个线程可以允许多次对同一对象上锁.对于每一个对象来说,Java虚拟机维护一个计数器,记录对象被加了多少次锁,没被锁的对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1.当计数器跳到0的时候,锁就被完全释放了
class MultiLock implements Runnable{ public static synchronized void count(int num){ System.out.println(num); } @Override public void run() { for (int i = 0; i < 10; i++) { count(i); } } } public class Test2 { public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new MultiLock()); exec.execute(new Runnable() { @Override public void run() { MultiLock.count(20);//需要等MultiLock释放锁 } }); exec.shutdown(); } }结果:
0 1 2 3 4 5 6 7 8 9 20当MultiLock释放锁之后,新任务才能获得锁。
2、显式使用Lock对象 java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式地创建、锁定和释放。必须放置在finally子句中带有unlock()的try-finally语句中。return语句必须在try子句中出现,以确保unlock()不会过早发生。显式地lock可以有机会去做清理的工作,维护正常的状态
private Lock lock = new ReentrantLock();//创建 public int next(){ lock.lock();//锁定 try { ++num; try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } return num; } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock();//释放 } return num; }Lock是一个接口, ReentrantLock是Lock的具体实现,可以尝试获取但最终为获取锁
public class ReenTrantLockTest { private ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { ReenTrantLockTest test = new ReenTrantLockTest(); new Thread(new Runnable() { //定义一个线程来获取锁,并休眠10s @Override public void run() { test.lock.lock(); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { //定义另一个线程尝试获取锁 @Override public void run() { boolean flag = false; try { flag = test.lock.tryLock(2, TimeUnit.SECONDS);//尝试获取锁,在2s后尝试失败 } catch (InterruptedException e) { e.printStackTrace(); }finally { if (flag) { test.lock.unlock();//有获取锁的话,释放 System.out.println("get lock"); }else { System.out.println("didn't get lock"); } } } }).start(); } }结果:
didn't get lock3、原子性与易变性 原子操作是不能被线程调度机制中断的操作。原子性可以应用在除long与double之外的所有基本类型之上的“简单操作”,对于读写long和double 之外的基本类型,可以保证它们是被当做不可分的操作来操作内存。 同步机制强制在处理器系统中,一个任务作出的修改必须在应用中是可视的,如果没有同步机制,那么修改的可视性将无法确定。 volatile关键字确保额应用中的可视性,将一个域声明为volatile的后,只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改。如果一个域完全由synchronized方法或语句来防护,那就不必将其设置为volatile的。
举个例子
public class VolatileTest { private static volatile boolean stop = false; //保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值 public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { int i = 0; @Override public void run() { while(!stop){ System.out.println(++i); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread.start(); Thread.sleep(10); stop = true; } }但是使用volatile时也下小心,比如下面的这种情况
private static volatile int num = 0; private static int nextNumber(){ return ++num; }包含了一个可原子访问的域,但是增量操作符(++)不是原子性的,它包含了两项工作,读取值,然后写回一个新值。如果第二个线程在第一个线程读取旧值和写回新值期间读到了这个域,则第二个线程和第一个线程看到了相同的值。 可以使用synchronized 来修饰nextNumber()方法。
private static synchronized int nextNumber(){ return ++num; }4、原子类 AtomicInteger, AtomicLong, AtomicReference等特殊的原子性变量类,他们提供下面形式的原子性条件更新操作
public final boolean compareAndSet(int expect, int update) { }Atomic类被设计用来构建java.util.concurrent中的类,因此只有在特殊的情况下才在自己的代码中使用它们,通常依赖锁要更安全一些。
5、线程本地存储 线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
class Accessor implements Runnable{ private int id; public Accessor(int id) { this.id = id; } @Override public void run() { while(!Thread.currentThread().isInterrupted()){//测试是否当前线程已被中断 ThreadLocalTest.increment();//不断增加 System.out.println(this); Thread.yield(); } } @Override public String toString() { return "#" + id + ":" + ThreadLocalTest.get(); } } public class ThreadLocalTest { private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){ Random random = new Random(); protected Integer initialValue() { return random.nextInt(100);//随机初始化值 }; }; //get和increment都不是synchronized的,因为ThreadLocal保证不会出现竞争条件 public static int get(){ return value.get(); } public static void increment(){ value.set(value.get() + 1); } public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 3; i++) { exec.execute(new Accessor(i)); } Thread.sleep(5);//sleep一下 exec.shutdownNow();//停止exec中所有的线程 } }输出:
#1:6 #0:25 #2:78 #0:26 #1:7 #0:27 #2:79 #0:28 #1:8 #2:80 #0:29 #2:81 #1:9 #0:30 #2:82 ...只能通过get()和set()方法来访问该对象的内容,get()方法将返回域其线程相关的对象的副本,set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。每个线程都被分配了自己的存储。