synchronized
先来了解synchronized,首先我们知道互斥同步是一种常见的高并发保障手段。同步是指多个线程并发的访问数据时,保证共享数据在同一时刻只能被一个线程访问,而互斥是同步的实现手段之一。
而被synchronized修饰的代码或代码块,在同一时刻只能被一个线程访问。
在java中同步锁是依赖对象存在的。不同线程对于同步锁是互斥的(例如 一个线程拿到对象的同步锁,那么另一个线程是拿不到的,除非这个线程释放同步锁)。
特点:
1,当两个并发的线程同步访问被synchronized修饰的方法或代码块时,只有一个线程能访问,另一个将被阻塞。
2,当一个线程访问“某对象”中被synchronized修饰的方法或代码块时,其他线程可以访问非同步的代码块。
3,当一个线程访问“某对象”中被synchronized修饰的方法或代码块时,其他线程访问其他被synchronized修饰的方法或代码块将被阻塞。
4,以上规则对其他对象锁同样适用。
实现synchronized
1锁住对象
class TextDemoThread1 implements Runnable{
public void run(){
synchronized (this) {//synchronized锁住this对象
for(int i = 0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"..执行.."+i);
}
}
}
}
public class TextDemo1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
TextDemoThread1 textDemo1 = new TextDemoThread1();
Thread thread = new Thread(textDemo1);
thread.setName("t1");
TextDemoThread1 textDemo2 = new TextDemoThread1();
Thread thread1 = new Thread(textDemo2);
thread1.setName("t2");
thread.start();
thread1.start();
}
}
执行结果:
t1..执行..0
t2..执行..0
t2..执行..1
t2..执行..2
t1..执行..1
t2..执行..3
t2..执行..4
t1..执行..2
t1..执行..3
t1..执行..4
2,锁住方法
class TextDemoThread1 implements Runnable{
public synchronized void run(){//锁住当前对象
for(int i = 0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"..执行.."+i);
}
}
}
public class TextDemo1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
TextDemoThread1 textDemo1 = new TextDemoThread1();
Thread thread = new Thread(textDemo1);
thread.setName("t1");
TextDemoThread1 textDemo2 = new TextDemoThread1();
Thread thread1 = new Thread(textDemo2);
thread1.setName("t2");
thread.start();
thread1.start();
}
}
运行结果
t1..执行..0
t2..执行..0
t2..执行..1
t2..执行..2
t2..执行..3
t2..执行..4
t1..执行..1
t1..执行..2
t1..执行..3
t1..执行..4
这两种加锁对象的区别是:
在给this对象加锁的时候,synchronized关键字经过编译之后,会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。在执行monitorenter时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器加一,相应的。执行monitorexit时会将锁计数器减1,当计数器为0时就释放锁。如果获取对象锁失败,那么当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。(Java线程是映射到操作系统的原生线程上的,如果要阻塞或者唤醒一个线程,都需要操作系统帮忙完成,这就需要从 用户态转换到核心态中,因此状态转换需要耗费很多的处理器转换时间)所以synchronized也被称作重量级锁。
但是将锁加到run()方法时不会产生这两个字节码指令。
同步方法是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置设为1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示做为锁对象。
volatile
`volatile只能添加在变量前,确保了变量的可见性和有序性,不保证操作的原子性(一次执行,不可分割);
public class Demo2 {
public static volatile int num = 0;
public static void main(String[] args) {
// TODO Auto-generated method stu
Thread[] thread = new Thread[20];
for(int i = 0;i<20 ;i++){
thread[i] = new Thread(new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
for(int j = 0 ;j<100;j++){
num++;
}
}
});
thread[i].start();
}
System.out.println(num);
}
代码执行结果按道理是2000,但是实际上结果比2000小???
这是因为num++不是原子操作,他是分为几步进行的,当他走了一步的时候,其他线程可能已经将值修改了,但被volatile修饰的变量为了保证能拿到正确的值,还是拿取前面的值,所以结果比2000小。
可见性
在汇编层会对volatile修饰的变量加lock前缀,而lock前缀其实是一个屏障,内存屏障会提供三个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入内存
- 如果是写操作,它会导致其他CPU中对应的缓存行无效
在修改变量的过程中
- 将修改变量的副本写入主内存
- 其他线程的 副本置为无效
读取时,先判断volatile修饰的变量是否有效,如果有效直接取,否则就去主内存去读取。
有序性
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯 定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也 不能把volatile变量后面的语句放到其前面执行