Java多线程复习与巩固(三)--线程同步
来源:互联网 发布:mac切换语言快捷键 编辑:程序博客网 时间:2024/06/09 03:49
多线程容易出现的问题
因为一个进程内,多个线程线程共享该进程的资源,而进程之间,资源的获取是互斥的,所以线程间通信比进程间通信更简单。我们可以直接通过共享资源的访问来实现线程间通信,这种通信方式十分有效(速度快),但也容易产生错误,如:线程干扰和内存一致性错误。
看一下下面这个例子:这个程序有两个线程,一个线程对计数器进行10000次加一操作,一个线程对计数器进行10000次减一操作,两个线程执行完后,计数器值原本应该等于0。但主线程在两个线程执行完后,打印计数器的值几乎很难得到0这个结果。
public class ThreadCommunicate { static class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } } static class IncrementTask implements Runnable { public void run() { for (int i = 0; i < 10000; i++) { counter.increment(); } } } static class DecrementTask implements Runnable { public void run() { for (int i = 0; i < 10000; i++) { counter.decrement(); } } } // Counter就是两个线程的共享资源 private static Counter counter = new Counter(); public static void main(String[] args) throws InterruptedException { // 创建两个线程同时对共享资源进行读写 Thread i = new Thread(new IncrementTask()); Thread d = new Thread(new DecrementTask()); i.start(); d.start(); i.join(); d.join(); System.out.println(counter.value()); }}
问题出现的原因
问题就出在c++
和c--
这两个操作上。
了解过汇编的都应该知道,一个c++
自增操作会分为以下几步:
mov eax,dword ptr [c] ;根据c的地址从内存取出c的值放到寄存器中 add eax,1 ;执行加一操作 mov dword ptr [c],eax ;把寄存器的值放回c地址所在的内存
另外从Java的反编译代码也可以看出来,c++
,c--
不止一步:
public void increment(); Code: 0: aload_0 1: dup 2: getfield #2 // 获取字段c的值 5: iconst_1 # // 获取常量1的值 6: iadd # // 执行加操作 7: putfield #2 // 把相加的结果放回字段c 10: return public void decrement(); Code: 0: aload_0 1: dup 2: getfield #2 // Field c:I 5: iconst_1 6: isub 7: putfield #2 // Field c:I 10: return
即使是非常简单的操作,在底层处理时也会分解成若干步。
不论是在单处理机CPU还是多处理机CPU种,两个线程执行指令的前后顺序是不确定的,如果出现下面的这种情况:
通过上图可以看出,两个线程同时执行一轮后,c的值结果等于-1。用一张动态图来形象描述一下这种情况:
很显然下面这张图才是我们想要的结果:
所以真正能得到等于0的序列只有两种:
这么多条指令的排列中只有2条能够得到0这个结果,难怪上面的程序几乎得不到0这个结果。
同步与异步
“同步”和“异步”,在各个领域中都有这两个词的出现。通俗的讲:同步就是在一条线上执行,异步就是分成多条线执行。
很显然对于上面的问题,我们应该要将两个线程并成一条线,让它按次序执行,这就是我们要讲的“线程同步”。先来看一张动态图来初步了解线程同步的基本原理:
使用线程同步解决问题
线程同步的方式有很多,下面我们介绍Java语言中最简单的线程同步的实现——使用synchronized
关键字。synchronized
关键字有两种使用方法:同步方法、同步代码块
同步方法
修改后的Counter类代码如下:
static class Counter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public int value() { return c; } }
因为
increment
方法和decrement
方法对共享资源进行了修改属于写操作,而value
方法没有进行修改仅仅只是读取属于读操作。对于读操作我们不需要加同步锁。
使用synchronized
关键字修饰的方法有以下特点:
首先,同一对象上的两个
synchronized
方法的调用不可能交织。当一个线程正在执行一个对象的synchronized
方法时,调用同一个对象的synchronized
方法的所有其他线程都会阻塞(挂起),直到第一个线程执行完同步方法。(解决了线程干扰问题)第二,当一个
synchronized
方法退出时,会与后续的synchronized
方法自动建立happens-before关联,在这一点上synchronized
与volatile
关键字功能有些类似:确保CPU寄存器或高速缓存内的数据能够及时写回内存,从而保证了对象状态的改变对所有线程是可见的。(解决了内存一致性问题)第三,
synchronized
对实例方法(非static方法)加同步锁,锁住的是实例对象(this)。synchronized
对类静态方法(static方法)加同步锁,锁住的是Class实例(Xxx.class)。// 锁住的是Xxx.classpublic static synchronized void function() {}// 锁住的是this实例对象public synchronized void function() {}
构造方法不能加同步锁,构造方法加synchronized
关键字产生语法错误。因为对构造方法加同步锁没有任何意义,两个线程可以同时创建同一个类的两个实例对象,这没有任何影响。
多线程的时候不要在构造方法中将this引用共享出去,可能会出现异常。比如你在构造方法中将this引用添加到集合中:
List.add(this)
,其他的线程就可以从集合中获取这个对象的引用,但这个对象并没有完成初始化,有的字段可能为null,这时就可能会发生空指针异常(也可能会发生其他运行时异常)。
同步代码块
还有一种方式是使用同步代码块的方式,这种方法与synchronized
方法在功能上基本一致。不同之处在于:同步代码块可以通过synchronized (xxx)
锁住任意对象,另外这种方法能有效的减小同步锁的粒度,避免了对大范围的代码加锁。代码如下:
static class Counter { private int c = 0; public void increment() { synchronized (this) { c++; } } public void decrement() { synchronized (this) { c--; } } public int value() { return c; } }
注意:如果
increment
和decrement
方法synchronized
锁住的不是同一个对象,就无法实现这两个方法的线程同步。
事实上只要锁住的是同一个对象就可以实现同步:
static class Counter { private Object lock = new Object(); private int c = 0; public void increment() { synchronized (lock) { c++; } } public void decrement() { synchronized (lock) { c--; } } public int value() { return c; } }
- Java多线程复习与巩固(三)--线程同步
- Java多线程复习与巩固(一)--线程基本使用
- Java多线程复习与巩固(六)--线程池ThreadPoolExecutor
- Java多线程复习与巩固(二)--线程相关工具类的使用
- Java多线程复习与巩固(四)--synchronized的实现
- Java多线程复习与巩固(七)--原子性操作
- java多线程(三) 线程的同步与通信
- Java线程同步与多线程
- Java多线程与线程同步
- java多线程与线程同步
- MFC多线程与线程同步 (三)
- java多线程三种方式区别,java多线程,线程同步方式,线程同步加锁的方法,wait与sleep区别
- Java多线程复习与巩固(八)--volatile关键字与CAS操作
- Java多线程(三)、线程同步
- Java多线程(三)、线程同步
- Java多线程(三)、线程同步
- Java多线程(三)、线程同步
- Java多线程(三)、线程同步
- linux 常用命令
- JavaScript 命名空间
- 管道基础
- jsp+Servlet实现上传
- codeblocks中怎么改变字体大小啊?
- Java多线程复习与巩固(三)--线程同步
- 51Nod--1015 水仙花数
- 重建二叉树
- 自己动手编写交叉编译工具链
- python简介
- 【CSS修改下拉选框select的默认样式】
- CSS中的相对定位和绝对定位
- Thrift源码解析(二)序列化协议
- maven命令整理