同步处理的引入
在java语言中存在两种内建的synchronized语法:synchronized代码块和synchronized方法( 静态方法和非静态方法)可以解决线程安全问题
首先synchronized将并行改为串行,当然会影响程序的执行效率,执行速度会受到影响。其次synchronized操作线程的堵塞,也就是由操作系统控制CPU的内核进行上下文的切换,这个切换本身也是耗时的。所以使用synchronized关键字会降低程序的运行效率。
造成线程安全问题的主要诱因
造成线程安全问题的主要诱因右两点:
- 一是存在共享数据(也成为临界资源)
- 二是存在多条线程共同操作共享数据
解决办法
当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。
- 在Java中每个对象都有一个对象锁,其实就是对象头上的一个标记值
- 同步处理的目标实际上就是实现线程排队执行的目的
解决线程安全问题有三种写法:
方法1:同步方法
针对临界资源的修改方法上添加同步约束–synchronized
//临界资源--被多个线程所共享操作的对象
public class OperNum{
private int target;//操作的数据
//针对操作数据的方法
public synchronized void add(){
target++;
System.out.println(Thread.currentThread().getName()+"add...."+target);
}
public synchronized void sub(){
target--;
System.out.println(Thread.currentThread().getName()+"sub...."+target);
}
原理:当一个线程在执行add方法时,其它线程不能执行add或者sub方法(同步方法都不能执行,因为是synchronized关键字会引入一个互斥锁,只有拥有锁的线程才能执行同步方法,其它线程只能阻塞等待),synchronized属于重入锁,即当前线程可以执行其它的synchronized方法,但是其它线程不能执行当前对象中的synchronized方法,可以执行没有synchronized约束的方法
方法2;同步代码块
锁为认为指定,推荐使用同步代码块方法
synchronized(锁){}
public class MyThread extends Thread {
private static int target = 0; // 操作目标,临界资源
private boolean flag = true; // 不是临界资源
// 定义一个互斥锁
private static Object lock = new Object();
public MyThread(boolean flag) {
this.flag = flag;
}
public void run() {
for (int i = 0; i < 50; i++) {
//synchronized (lock) 可以保证{}中的代码执行具备原子性
synchronized (lock) {
if (flag){
target++;
}else {
target--;
}
System.out.println(Thread.currentThread().getName() + (flag ? "add" : "sub") + "...." + target);
}
}
}
public static void main(String[] args) {
for(int i=0;i<4;i++) {
new MyThread(i%2==0).start();
}
}
}
方法3:同步静态方法
以当前类对象充当锁,所有静态方法互斥
- 和非静态方法不互斥,因为非静态同步方法的锁是当前对象,不是类锁
//临界资源
public class OperNum {
private static int target=10;//操作的数据
//针对操作数据的方法
public synchronized static void add() {
target++;
System.out.println(Thread.currentThread().getName()+"add...."+target);
}
public synchronized static void sub() {
target--;
System.out.println(Thread.currentThread().getName()+"sub...."+target);
}
}
理解:
在方法上添加synchronized关键字后就可以保证在一个时刻上只有一个线程在调用某个方法(锁只能有一个),不会出现并发的情形,达到排队执行的效果。
- 在Java中synchronized可保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),这点确实也是很重要的。
- 常见的4种Java线程锁:原子量AtomicInteger、信号量Semaphone、同步处理synchronized和重入锁ReentrantLock
- jdk6之前是重量级锁,JDK6开始优化为锁的状态总共有四种,无锁状态(没有synchronized)、偏向锁、轻量级锁和重量级锁。锁状态的改变是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁, 在重度竞争的情况下,会升级到重量级锁。 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说锁只能从低到高升级,不会出现锁的降级。
注意:
- 临界资源对象中的同步方法和非同步方法不互斥,所以可以同时运行。
- 临界资源对象中的同步方法(两个方法都有synchronized则有等待)是互斥的,所以只能获取锁的线程运行,另外一个处于阻塞等待。
- static方法使用的是类锁,而非静态方法使用的是对象锁。
- 如果两个方法都是静态的,则使用的都是类锁,所以会有等待。
- wait方法会有释放锁,而sleep不释放锁。
同步实例方法
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
方法上添加synchronized就叫做同步方法,例如public synchronized void draw(double amount){}
注意:在一个类对象中所有的同步方法是互斥的
- 只要有一个线程进行了当前类对象(只能一个对象)的同步方法,则不允许其它线程在进入当前这个对象的任何同步方法,但是允许进入非同步方法
- 同样当前线程则可以进入当前类对象的其它同步方法,也允许进入非同步方法,当线程进入同步方法,则获取同步锁,离开同步方法则释放同步锁
- 这个锁就是当前类对象
这种方法不是最佳选择,因为这里的同步处理颗粒度太大了(所有当前对象中的同步处理方法都是互斥的),会影响并发性
线程安全的类:是通过使用同步方法的类,同步监视器是this
- 该类的对象可以被多个线程安全地访问
- 每个线程调用该对象的任意方法之后都将得到正确结果
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态
StringBuilder和StringBuffer
- StringBuilder是线程不安全的类,数据不安全,但是并发执行效率高,一般用于定义临时变量
- StringBuffer是线程安全的类,数据安全,但是并发执行效率差,一般用于定义属性
同步静态方法
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
同步监视器是当前类对象Class c=Date.class
public class App {
private static int num = 0;
public static synchronized void add() {
System.out.println(Thread.currentThread().getName()+":begin...."+new Date());
int cc = num;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cc++;
num = cc;
System.out.println(cc);
System.out.println(Thread.currentThread().getName()+":end...."+new Date());
}
public static void main(String[] args)throws Exception {
Thread[] ts=new Thread[10];
for(int i=0;i<ts.length;i++) {
Runnable r=new Runnable() {
public void run() {
App app=new App();
app.add();
}
};
ts[i]=new Thread(r);
ts[i].start();
}
for(Thread temp:ts) {
temp.join();
}
System.out.println("Main:"+num);
}
}
注意:如果在方法上去掉static,则不能实现锁定效果,因为是对象锁,而在线程中创建了10个对象,各个对象没有任何关系,所以不能达到锁定的效果,但是类锁的效果仍旧有效。
同步方法块
上面的两种方式比较死板,普通方法同步是以当前对象作为锁,静态方法同步是以当前类对象作为锁
所以引入更为灵活的方式:同步块,将锁对象作为参数进行传递
synchronized(account){}同步监视器可以阻止多个线程对同一个共享资源的并发访问,任何时刻只有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放堆该同步监视器的锁定。
注意:在使用锁的过程中注意wait的使用问题,否则可能会导致java.lang.IllegalMonitorStateException
synchronized底层语义原理
对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。而synchronized方法则会被翻译成普通的方法调用和返回指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象
使用synchronized加锁的字节码会出现monitorenter和monitorexit两个指令,可以理解为代码块执行前的加锁和退出同步时的解锁
- 执行monitorenter指令时,线程会为锁对象关联一个ObjectMonitor对象。
- 线程遇到synchronized同步时,先会进入ObjectMonitor对象的EntryList队列中,然后尝试把ObjectMonitor对象的owner变量设置为当前线程,同时ObjectMonitor对象的monitor中的计数器count加1,即获得对象锁。否则通过尝试自旋一定次数加锁,失败则进入ObjectMonitor对象的cxq队列阻塞等待
- synchronized是可重入,非公平锁,因为entryList的线程会先自旋尝试加锁,而不是加入cxq排队等待,不公平
何时需要同步
在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。
对于非静态字段中可更改的数据,通常使用非静态方法访问。
对于静态字段中可更改的数据,通常使用静态方法访问。
释放同步锁
- 同步代码执行结束
- 同步方法中遇到break或return终止代码