首先,我们要搞清楚为什么要学习并发编程?主要有以下三点:
第一点:面试非常重要
企业面试程序员标准,考察因素:
1.考察我公司技术你是否熟悉50%以上,或者我们公司有特殊的技术需求,正好你熟悉,那么可能会考虑录用你。
2.细节、态度、人品问题。(1、2条件满足基本上就会录用你)
3.知识面、潜力(这是加分项,如果能把并发的一些顶层原理都能说清楚的话,肯定能给面试官不错的印象,面试就基本上成功了)
第二点:对自己的技术提升很有帮助
这也是我们最受益的一点,就是你并发编程技术学到手了,有了一个知识面的扩展,眼界更宽了。
第三点:触类旁通
如果你学好了并发编程,在以后的分布式系统中,你都可以找到类似并发、分布式、并行处理问题的概念。
接着,我们要明确该如何学习并发编程?
在公司里面其实很多JAVA程序员,亦或是所谓的技术Leader,他们可能知道多线程中有synchronized、volatile、ReentrantLock、concurrent下数据包等等...这些看似高深的代名词,但是不等于他们就会懂得如何去使用,滥用的结果往往需要自己承担相应的后果。其实并发编程没有我们想象的那么复杂,我们只需要掌握最基本的概念就可以很轻松的入门,然后从中剖析这些概念的本质,结合实际业务逻辑去应用上去,那么你就会成为并发编程方面的专家。
接着我们来学习什么是线程安全
线程安全概念:当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
提到线程安全就不得不提到synchronized,接触过多线程的同学肯定对它都不陌生,它可以在任意对象及方法上加锁,而加锁的这段代码成为"互斥区"或"临界区"。
示例一:多个线程一个锁
下面我们便来看一个最简单的多线程的例子
package com.internet.thread;
public class MyThread extends Thread{
//定义变量的初始值为5
private int count=5;
//synchronied加锁
public synchronized void run () {
count--;
System.out.println(this.currentThread().getName()+"count="+count);
}
public static void main(String[] args){
/**
* 分析:当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的)
* 一个线程想要执行synchronized修饰的方法里的代码:
* 1 尝试获得锁
* 2 如果拿到锁,执行synchronized代码体内容。拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止
* 而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题)
*/
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
Thread t3 = new Thread(myThread,"t3");
Thread t4 = new Thread(myThread,"t4");
Thread t5 = new Thread(myThread,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
上面的代码是加了锁的(在定义run方法时加了synchronized关键字 ),这时运行main方法是没有线程问题的,如下图所示。我们可以看到线程执行的顺序是1、4、5、3、2,而不是我们书写的1、2、3、4、5,这边是我们上面说的排队是按照CPU分配的顺序先后顺序而定。
如果我们把run方法的synchronized关键字去掉,这时便会有线程安全问题,运行结果如下所示,可以看到前两个数字竟然一样,这说明此时是线程不安全的。
t1count=3
t2count=3
t4count=2
t3count=1
t5count=0
示例总结:当多个线程访问myThread的run方法是,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的),一个线程想要执行synchronized修饰的方法里的代码,首先是尝试获得锁,如果得到锁,执行synchronized代码体内容,拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题,锁竞争问题对于线程比较少的情况倒是影响不大,但是如果线程很多的话,同一时间有很多线程要抢夺一把锁,CPU会瞬间达到很高的占用率,很有可能就宕机了,其实这和我们双十一抢购商品是一样的,在0点0分0秒瞬间有几十万几百万的请求过来,这对于一般的服务器来说,瞬间就崩溃了,因此我们尽量避免 锁竞争的问题)
多个线程多个锁:多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。
新建类MultiThread,如下所示。
package com.internet.thread;
public class MultiThread {
//定义一个静态变量num,并附初始值为0
private int num = 0;
public synchronized void printNum(String tag){
try {
if(tag.equals("a")){
num = 100;
System.out.println("tag a, set num over!");//打印给num赋值完毕
Thread.sleep(1000);//休息1秒,之所以这样是为了让大家看到两个线程互不干扰,如果不休息的话,瞬间执行完了,看不出效果
}else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = "+num);
} catch (Exception e) {
e.printStackTrace();
}
}
//注意观察run方法输出顺序
public static void main(String[] args){
//两个不同的对象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}
我们运行该类,会看到如下结果,可以看到线程t1和t2互不影响,各自运行各自的,我们让t1线程休息了1秒钟,这样t2线程执行完了,t1线程才执行完。synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,所以上面代码中哪个线程先执行synchronized关键字的方法,那个线程就持有该方法所属对象的锁(Lock),两个对象,线程获得的就是两个不同的锁,他们互不影响。
有一种情况则是相同的锁,那就是在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)。为了看效果,我们在变量num前面加static关键字, 给printNum方法加上static关键字,如下图所示。
现在我们再运行main方法,结果如下所示,这时num和printNum便排队执行了,先执行完线程t1之后再执行线程t2。
tag a, set num over!
tag a, num = 100
tag b, set num over!
tag b, num = 200
下面我们聊聊对象锁的同步和异步
同步:synchronized
同步的概念就是共享,我们要牢牢记住"共享"这两个字,如果不是共享的资源,就没有必要进行同步(举个简单例子,我们存款和取款,一定要同步,因为不同步的 话,资金就乱了,这是不能忍受的)。
异步:asynchronized
异步的概念就是独立,相互之间不受到任何制约。就好像我们学习http的时候,在页面发起的Ajax请求,我们还可以继续浏览或操作页面的内容,二者之间没有任何关系。
同步的目的就是为了线程安全,其实对于线程安全来说,需要满足两个特性:原子性(同步)和可见性。
我们还是来一起看个例子。
示例三
新建MyObject类,如下所示:
package com.internet.thread;
public class MyObject {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(4000);
} catch (Exception e) {
e.printStackTrace();
}
}
public void method2(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args){
final MyObject mo = new MyObject();
/**
* 分析:
* t1线程先持有object对象的Lock锁,t2线程可以以异步的方式调用对象中的非synchronized修饰的方法
* t1线程先持有object对象的Lock锁,t2线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步
*/
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mo.method1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
mo.method2();
}
},"t2");
t1.start();
t2.start();
}
}
运行上面的main方法,我们会看到t1和t2同时打印出来了,这说明此时method1和method2是异步执行的。
现在我们给method2方法也加上synchronized关键字
public synchronized void method2(){
System.out.println(Thread.currentThread().getName());
}
再运行main方法,这回我们看到t1信息输出4秒后才会看到t2信息,出现这种情况的原因是,我们只new了一个对象,而synchronized关键字锁的便是对象,由于method1和method2现在都被synchronized关键字修饰,因此只要是访问该对象的这两个方法的线程都会进行同步操作,也就是说谁先拿到对象的锁谁便先执行,执行完之后,释放锁,这时下一个线程才能执行。
总结下示例三:
A线程先持有object对象的Lock锁,B线程如果在这个时候调用对象中的同步(synchronized)方法则需要等待,也就是同步。
A线程先持有object对象的Lock锁,B线程可以以异步的方式调用对象中的非synchronized修饰的方法。