一、多线程的并发与并行:
并发:多个线程同时都处在运行中的状态。线程之间相互干扰,存在竞争,(CPU,缓冲区),每个线程轮流使用CPU,当一个线程占有CPU时,其他线程处于挂起状态,各线程断续推进。
并行:多个线程同时执行,但是每个线程各自有自己的CPU,不存在CPU资源的竞争,他们之间也可能存在资源的竞争。
并发发生在同一段时间间隔内,并行发生在同一时刻内。并发执行的总时间是每个任务的时间和,而并行则取决于最长任务的时间。
下面看一下A,B两个任务在并行和并发情况下是怎么执行的:[不考虑其他资源竞争,只考虑CPU竞争]
A任务:a+b+c,
A任务有三步:a=1+1,b=2+2,c=3+3
B任务:x+y+z,
A任务有三步:x=1*1,y=2*2,z=3*3
A,B并行操作:多核CPU,多任务并行。
CPU1:
CPU2:
图中可以看到A,B操作相互不受影响。
当A任务开始的时候B任务也开始,他们可以同一时刻开始,如果每一个小任务耗时相同,那么他们可能同时结束。
A,B并发操作:单核cpu,多任务并发。
图中可以看出A,B同时执行的时候,一定有一个先,有一个后,因为CPU只有一个执行了A就不能执行B,但是为了让两个任务能够“同时完成“,CPU先执行A的一部分,在执行B的一部分,当这个间隔时间非常短的时候我们看到的就是A,B都在运行。
举个简单的例子:
左右手同时握住两支笔,并排点一个点,点出一条线来,这就是并行。只有一只手握住一支笔,左点一下,右点一下知道画出两条线,这两条线看似同时开始同时结束,实际是有时间差的,这就是并发。
实际操作中并不是严格的并发或者是并行,因为CPU有限,而任务是无限的。任务数量超过CPU核心数量的时候,就是并发并行共同存在的时候,不过这不是我们关注的重点,CPU资源的分配问题,是操作系统关心的重点。我们关心的重点是任务之间发生的资源竞争问题。
当多个线程对同一个资源进行访问和操作的时候就会出现数据一致性问题。一致性问题得不到解决多个任务的操作永远得不到正确的结果,解决一致性问题的方法就是同步机制。
二、synchonrized实现同步:
java每个对象都有一个内置锁,当用synchonrized修饰方法或者代码块的时候就会调用这个锁来保护方法和代码块。
同步方法:同步静态方法,同步非静态方法。
public synchronized void addMeethod2(){
num2++;
}
public static synchronized void addMeethod2(){
num2++;
}
同步代码块:
synchronized(Object){...}:Object表示一个对象,synchronized获取这个对象的同步锁,保证线程获取object的同步锁之后其他线程不能访问object的任何同步方法。但其他线程可以访问非同步的部分。
public void addMeethod3(){
synchronized(this){
num3++;
}
}
public void addMeethod4(){
synchronized(num4){
num4++;
}
}
public void addMeethod5(){
synchronized(Learnlocks.class){
num5++;
}
}
同步当前对象this:public void addMeethod3(){synchronized(this){num3++;}}和同步非静态方法:public synchronized void addMeethod2(){ num2++;}他们的效果是一样的都是作用与当前对象,即获取的object都是当前对象。那么只有这个线程可以操作这个对象所有synchronized修饰的部分,执行完这部分内容才会释放锁,其他线程才能访问。
下面看那一下示例:模拟三个线程分别对不同的同步机制保护的数据进行操作,看他们的具体表现。
import java.util.concurrent.atomic.AtomicInteger;
public class NumAction {
private Integer num1=0;
private Integer num2=0;
private Integer num3=0;
private Integer num4=0;
private Integer num5=0;
private volatile Integer num6=0;
private AtomicInteger num7=new AtomicInteger(0);
public NumAction() {
}
//省略get/set方法public void Initializers(NumAction lk){
lk.num1=0;
lk.num2=0;
lk.num3=0;
lk.num4=0;
lk.num5=0;
lk.num6=0;
lk.num7=new AtomicInteger(0);
}
//----------------------------------------------------------重点部分------------------------------------------------------
public void addMeethod1(){
num1++;
}
public synchronized void addMeethod2(){
num2++;
}
public void addMeethod3(){
synchronized(this){
num3++;
}
}
public void addMeethod4(){
synchronized(num4){
num4++;
}
}
public void addMeethod5(){
synchronized(NumAction.class){
num5++;
}
}
public void addMeethod6(){
num6++;
}
public void addMeethod7(){
num7.incrementAndGet();
}
public void Add100() {
for (int i = 0; i < 100; i++) {
addMeethod1();
addMeethod2();
addMeethod3();
addMeethod4();
addMeethod5();
addMeethod6();
addMeethod7();
}
}
}
NumAction类:有七个属性,八个方法,前七个方法分别给七个属性自增一次。第八个方法是调用这七个方法100次。下面用多个线程来执行这个方法。
package com.eshore.ljcx.locks;
public class Learnlocks2 extends Thread{
private static NumAction numaction=new NumAction();
public void run() {
numaction.Add100();
}
public static void testRun(){
Learnlocks2 l1 = new Learnlocks2();
Learnlocks2 l2 = new Learnlocks2();
Learnlocks2 l3 = new Learnlocks2();
new Thread(l1).start();
new Thread(l2).start();
new Thread(l3).start();
try {
Thread.sleep(7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(numaction.getNum1()+","+numaction.getNum2()+","+numaction.getNum3()+","+numaction.getNum4()+","+numaction.getNum5()+","+numaction.getNum6()+","+numaction.getNum7());
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
testRun();
numaction.Initializers(numaction);
}
}
}
我们启动了三个线程去执行给每个数据自增一百次的操作,理想状态下最终每个数据应该是300,再将这个步骤重复10次,看一下是不是一样的结果。
结果如下:num2,num3,num5,num7是300,其他的数据都不正确。说明这四个数据的同步起到了作用,他们分别是:(同步方法,同步当前对象,同步类对象,Atom原子对象。)而volatitle关键字被称为轻量级的同步机制并没有起到应有的效果。同步属性对象num4也并没有作用。
299,300,300,300,300,299,300
297,300,300,300,300,300,300
292,300,300,297,300,297,300
275,300,300,296,300,294,300
298,300,300,300,300,300,300
283,300,300,300,300,297,300
297,300,300,300,300,300,300
297,300,300,300,300,297,300
299,300,300,300,300,300,300
296,300,300,299,300,298,300
同步num4为什么没有起到作用:因为实际上我们上锁的是对象本身,并不是对象的引用。每次给对象自增,对象已经修改了,那么我们每次获取的锁也不是同一个锁。自然没有办法保持同步。如果我们添加一个不会被修改的属性对象num0。
private Integer num0 =0;
修改方法四:
public void addMeethod4(){
synchronized(this.num0){
num4++;
}
}
结果如下:发现num4的输出结果也是预期的300.
296,300,300,300,300,300,300
297,300,300,300,300,300,300
248,300,300,300,300,281,300
255,300,300,300,300,289,300
297,300,300,300,300,300,300
289,300,300,300,300,298,300
298,300,300,300,300,300,300
290,300,300,300,300,300,300
263,300,300,300,300,299,300
296,300,300,300,300,300,300
从num4可以看出来我们获取num0的对象锁,但是num4却可以保持同步。这是因为num0这个对象锁的代码块是num0对象锁的作用域,要对num4操作,必须获取num0对象锁。很多时候误区可能在于我们要对那个数据保持同步就要获取那个数据的锁,这是很错误的理解。首先synchonrized获取的是对象锁,数据不是对象就满足不了条件(个人见解,仅供参考)。
总结下:(synchonrized的用法在示例代码部分已经做了展示。)
//-------------------------------------------------------update:2017/3/23-------------------------------------------------
对于同步方法:
1、所有静态方法M0,M1。。:锁定的对象是类
多线程用类使用M0,M1会阻塞:M0,M1属于类,类加锁,锁相同
2、所有非静态方法M2,M3:锁定的对象是类的实例
多线程用不同的对象使用M2不会阻塞:对象不同,锁不同
多线程用相同的对象使用M2会阻塞:对象相同,锁相同
多线程用相同的对象使用M2,M3会阻塞:对象相同,锁相同
多线程用不同的对象使用M2,M3不会阻塞:对象不同,锁不同
*&*:
多线程访问M1(静态方法)和M2(非静态方法)不会阻塞:M1的锁是类,M2的锁是对象,锁不同
对于同步代码块:
1、非静态方法代码块:锁定的是指定的对象Object
同步代码块当Object==this的时候,和同步方法效果相同,但是如果只作用了整个方法的一部分代码块,那么效率会更高。【作用内容大小带来的差异】
同步代码块当Object==X(X==指定一个不变的对象 final Integer X=0)的时候,和同步方法效果不同,不同点在于此时作用域是所有以X为锁的代码块,而同步方法会作用于对象下所有的同步方法。这也是同步代码块相较于同步方法效率会更高的原因。【作用方法的数量带来的差异】
2、静态方法代码块:锁定的是指定的对象Object
同步代码块当Object==this的时候,不多说,还是类(静态方法类调用,怎么也扯不到实例对象上去)
同步代码块当Object==X(X==指定一个不变的对象 static final Integer X=0,X必须是静态的,因为静态方法只能使用静态属性)的时候,锁的作用域是所有使用X作为锁的代码块。而同步静态方法作用域是所有的同步方法。
//-------------------------------------------------------------------------------------------------------------------------
并发机制提高任务效率,而同步机制是保证并发的安全,但是同时也会破坏并发效率,所以同步的锁的粒度把握是个关键问题,在保证同步的情况下尽量保证性能才是关键。
对于非静态字段中可更改的数据,通常使用非静态方法访问.
对于静态字段中可更改的数据,通常使用静态方法访问,也可用非静态访问。不过是操作类。