首页 > 代码库 > 多线程爬坑之路--并发,并行,synchonrized同步的用法
多线程爬坑之路--并发,并行,synchonrized同步的用法
一、多线程的并发与并行:
并发:多个线程同时都处在运行中的状态。线程之间相互干扰,存在竞争,(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() {
}
public Integer getNum1() {
return num1;
}
public void setNum1(Integer num1) {
this.num1 = num1;
}
public Integer getNum2() {
return num2;
}
public void setNum2(Integer num2) {
this.num2 = num2;
}
public Integer getNum3() {
return num3;
}
public void setNum3(Integer num3) {
this.num3 = num3;
}
public Integer getNum4() {
return num4;
}
public void setNum4(Integer num4) {
this.num4 = num4;
}
public Integer getNum5() {
return num5;
}
public void setNum5(Integer num5) {
this.num5 = num5;
}
public Integer getNum6() {
return num6;
}
public void setNum6(Integer num6) {
this.num6 = num6;
}
public AtomicInteger getNum7() {
return num7;
}
public void setNum7(AtomicInteger num7) {
this.num7 = num7;
}
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
同步一个对象(类对象即类,类的实例对象,类的属性对象)的时候:
锁的作用域有两部分:一部分是这个对象的所有同步方法,另一个部分是这个synchonrized修饰的部分。从num4可以看出来我们获取num0的对象锁,但是num4却可以保持同步。这是因为num0这个对象锁的代码块是num0对象锁的作用域,要对num4操作,必须获取num0对象锁。很多时候误区可能在于我们要对那个数据保持同步就要获取那个数据的锁,这是很错误的理解。首先synchonrized获取的是对象锁,数据不是对象就满足不了条件(个人见解,仅供参考)。
总结下:(syconrized的用法在示例代码部分已经做了展示。)
- 同步分两种:同步代码块,同步方法(同步静态方法,同步非静态方法),
- synchonrized(object){}:所有的非静态同步方法用的都是同一把锁—--—实例对象本身。
- object==this则对象锁是当前对象的锁。
- object==Xxx.class则对象锁是当前对象所属类对象的锁。
- object==Object则对象锁是当前Object对象的锁(num0是个Integer类的实例对象)。
- 而所有的静态同步方法用的也是同一把锁—--—类对象本身。
- 并发机制提高任务效率,而同步机制是保证并发的安全,但是同时也会破坏并发效率,所以同步的锁的粒度把握是个关键问题,在保证同步的情况下尽量保证性能才是关键。
- 对于非静态字段中可更改的数据,通常使用非静态方法访问。
- 对于静态字段中可更改的数据,通常使用静态方法访问。
参考:http://www.cnblogs.com/csniper/p/5478572.html
多线程爬坑之路--并发,并行,synchonrized同步的用法