咱们对并发一词并不陌生,它一般指多个任务同时执行。实际上这不彻底对,“并行”才是真正意义上的同时执行,而“并发”则更偏重于多个任务交替执行。有时候咱们会看见一些人一边嘴里嚼着东西一边讲话,这是并行;固然,更文明礼貌的方式是讲话前先把嘴里的东西咽下去,这是并发。并发早期被用来提升单处理器的性能,好比I/O阻塞。在多核处理器被普遍应用的今天,并行和并发的概念已经被模糊了,或许咱们没必要太过纠结两者之间的微妙差异。java
Java的并发是经过多线程实现的,若是有多个处理器,线程调度机制会自动向各处理器分派线程。线程不一样于进程,它的级别比进程更低,一个进程能够衍生出多个线程。现代操做系统都是多进程的,不一样的程序分属于不一样的进程,各进程之间不会共享同一块内存空间。由于进程之间没有交集,因此各进程可以相安无事地运行,这就比如同一栋楼里的不一样住户,你们关起门来各过各的,别人家夫妻吵架跟你一点关系都没有。计算机中运行的各个程序都分属于不一样的进程,你在使用IDE时没必要担忧播放器会修改你的代码,也不会担忧通信软件会对IDE有什么影响。可是到了多线程,一切都变得复杂了,原来不一样的住户如今要搬到一块儿合租,卫生间、厨房都变成了公用的。每一个线程都共享其所属进程的资源,多线程的困难就在于协调不一样线程所驱动的任务之间对共享资源的使用。程序员
既然多线程这么困难,为何不直接使用多进程呢?一个缘由是进程及其昂贵,操做系统会限制进程的数量。另外一个缘由来自遥远的蛮荒年代,当时一些中古系统并不支持多进程,java为了实现可移植的目的,用多线程实现了并发。面试
Java的多线程无处不在,然而实际状况是,不多有人真正编写过并发代码,实际上有至关多的技术人员从未写过真正意义上的并发。缘由是一些诸如Servlets的框架帮助咱们处理了并发问题。设计模式
Java的线程是经过Runnable接口实现的,能够这样实现一个线程:安全
1 class Task implements Runnable { 2 private int n = 1; 3 private String tName = ""; 4 5 public Task(String tName) { 6 this.tName = tName; 7 } 8 9 @Override 10 public void run() { 11 while(n <= 10) { 12 System.out.print(this.tName + "#" + n + " "); 13 n++; 14 } 15 System.out.println(this.tName + " is over."); 16 } 17 } 18 19 public class C_1 { 20 public static void main(String[] args) { 21 Task A = new Task("A"); 22 Task B = new Task("B"); 23 A.run(); 24 B.run(); 25 System.out.println("main is over."); 26 } 27 }
运行结果与顺序执行没什么不一样:多线程
这说明实现了Runnable的类实际上与普通类没什么不一样,它充其量只是个任务,想要实现并发,必须把任务附着在一个线程上:并发
1 public class C_1 { 2 public static void main(String[] args) { 3 Thread t1 = new Thread(new Task("A")); 4 Thread t2 = new Thread(new Task("B")); 5 t1.start(); 6 t2.start(); 7 System.out.println("main is over."); 8 } 9 }
此次才是真正意义上的并发:框架
start()会为线程启动作好必要的准备,以后调用任务的run()方法,让任务运行在线程上。在JDK1.5以后加入了线程管理器,能够没必要显示地把任务附着在线程上,同时线程管理器还会自动管理线程的生命周期。ide
1 public class C_1 { 2 public static void main(String[] args) { 3 ExecutorService es = Executors.newCachedThreadPool(); 4 es.execute(new Task("A")); 5 es.execute(new Task("B")); 6 es.shutdown(); 7 System.out.println("main is over."); 8 } 9 }
shutdown()方法用于阻止向ExecutorService中提交新线程。若是在es.shutdown()时候仍然提交新线程,将会抛出java.util.concurrent.RejectedExecutionException。性能
JDK8以后加入了lambda表达式,对于一些短小的不须要重用的任务,能够没必要单独写成一个类:
1 public class C_1 { 2 public static void main(String[] args) { 3 ExecutorService es = Executors.newCachedThreadPool(); 4 es.execute(new Task("A")); 5 es.execute(new Task("B")); 6 es.execute(new Runnable() { 7 @Override 8 public void run() { 9 System.out.println("I am in lambda."); 10 } 11 }); 12 es.shutdown(); 13 System.out.println("main is over."); 14 } 15 }
因为每一个lambda表达式的初始化都会耽误一点时间,所以在执行短小的运行速度很快的多线程程序时,这种方式每每看不出效果,程序更像是顺序的。
咱们常常说某个方法是线程安全的。我并不以为“线程安全”是个易于理解的词。简单地说,若是某个方法是“线程安全”的,那么这个方法在多线程环境下的运行结果也将是可预期的。
1 import java.util.concurrent.ExecutorService; 2 import java.util.concurrent.Executors; 3 4 class Task2 implements Runnable { 5 String tName = ""; 6 7 public Task2(String tName) { 8 this.tName = tName; 9 } 10 11 @Override 12 public void run() { 13 for(int i = 1; i <= 10; i++) { 14 System.out.print(tName + "#" + i + " "); 15 } 16 } 17 } 18 19 public class C_2 { 20 public static void main(String[] args) { 21 ExecutorService es = Executors.newCachedThreadPool(); 22 es.execute(new Task2("A")); 23 es.execute(new Task2("B")); 24 es.shutdown(); 25 } 26 }
运行结果多是:
做为一个任务,Task2每次运行都会将10个编号依次打印出来,尽管每次打印的顺序可能有所区别,但咱们仍然认为它是可预期的,是线程安全的。
Task2之因此安全,是由于它没有共享的状态。若是加入状态,就很容易把一个本来线程安全的方法变成不安全。
1 class Task2 implements Runnable { 2 String tName = ""; 3 static int no = 1; 4 5 public Task2(String tName) { 6 this.tName = tName; 7 } 8 9 @Override 10 public void run() { 11 for(int i = 1; i <= 10; i++) { 12 System.out.print(tName + "#" + i + " "); 13 no++; 14 } 15 } 16 }
这里仅仅是对Task2稍加修改,让两个任务共享同一个序号,每次执行循环时都会对no加1。咱们预期的效果是每次打印出不一样的no值,然而实际的运行结果多是:
出现了A#9和B#9。其缘由是两个线程同时对no产生了竞争,而no++并又是经过多条指令完成的。在no=9时,A线程将其打印出来,以后执行++操做,在执行到一半的时候B进来了,因为++操做并未结束,所以B看见的还是上一状态。
无状态的程序必定是线程安全的。HTTP是无状态的,处理HTTP请求的servlet也是无状态的,所以servlet是线程安全的。尽管如此,你仍需时刻保持警戒,由于没有任何约束阻止你把一个本来无状态的方法变成有状态的。
1 public class MyServlet extends HttpServlet { 2 3 private static int no = 1; 4 5 @Override 6 protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException { 7 arg0.setAttribute("no", no++); 8 } 9 }
有了共享就有了竞争,此时本来的线程安全也将变成不安全。
我曾经面试过不少程序员,问他们知道哪些经常使用的设计模式,不少人的第一个回答就是单例模式,可见单例模式的深刻人心。下面是个典型的单例。
1 public class Singleton { 2 private static Singleton sl = null; 3 4 private Singleton() { 5 System.out.println("OK"); 6 } 7 8 public static Singleton getInstance() { 9 if(sl == null) 10 sl = new Singleton(); 11 return sl; 12 } 13 14 public static void main(String[] args) { 15 Singleton.getInstance(); 16 Singleton.getInstance(); 17 Singleton.getInstance(); 18 }
Singleton在执行初始化后会打印OK,因为Singleton只会执行一次初始化,所以程序最终仅仅会打印一次OK。然而一切在多线程中变得就不一样了。把单例放在线程中:
1 class Task3 implements Runnable { 2 3 @Override 4 public void run() { 5 Singleton.getInstance(); 6 } 7 } 8 9 public class Singleton { 10 private static Singleton sl = null; 11 12 private Singleton() { 13 System.out.println("OK"); 14 } 15 16 public static Singleton getInstance() { 17 if(sl == null) 18 sl = new Singleton(); 19 return sl; 20 } 21 22 public static void main(String[] args) { 23 ExecutorService es = Executors.newCachedThreadPool(); 24 es.execute(new Task3()); 25 es.execute(new Task3()); 26 es.execute(new Task3()); 27 es.shutdown(); 28 } 29 }
3个线程同时发现了sl==null,此时可能会执行3次初始化,打印3次OK。这也成为单例模式被人诟病的缘由,虽然能够经过双检查锁和volatile关键字解决上述状况,但代码较为复杂,性能也让人捉急。一个好的方式是使用主动初始化代替单例:
1 public class Singleton_better { 2 3 private static Singleton_better sl = new Singleton_better(); 4 5 public static Singleton_better getInstance() { 6 return sl; 7 } 8 9 public Singleton_better() { 10 System.out.println("OK"); 11 } 12 }
另外一种方式是惰性初始化, 它在解决了线程安全的同时还保留了单例的优势:
1 public class Single_lazy { 2 3 private static class Handle { 4 public static Single_lazy sl = new Single_lazy(); 5 } 6 7 public static Single_lazy getInstance() { 8 return Handle.sl; 9 } 10 }
做者:我是8位的
出处:http://www.cnblogs.com/bigmonkey
本文以学习、研究和分享为主,如需转载,请联系本人,标明做者和出处,非商业用途!
扫描二维码关注公做者众号“我是8位的”