synchronized
用法:修饰方法或者修饰代码块
1、修饰方法
1.1、修饰普通方法
1.2、修饰静态方法
2、修饰代码块
性质:
1、具有原子性和可见性
2、同步方法可以调用另外一个同步方法(synchronize是可重入锁)
3、非同步方法和同步方法可以同时调用
4、子类的同步方法可以调用父类的同步方法
4、synchronized遇到异常默认是释放锁资源(并发情况需要特别留意)
使用优化:
1、同步代码越少越好,只锁涉及到同步的部分(提高效率)
2、锁对象的属性发生改变,不会影响锁的使用,但是如果将引用指向另外的对象,那么锁就会发生改变——避免将锁对象的引用指向另外的对象
3、不要以字符串常量作为锁定对象
用法:
1、修饰普通方法——相当于修饰代码块(锁定的是this对象)
说明:每个对象都有一个内置锁,对一个对象加锁,线程在执行的时候,去堆内存中请求该对象的锁(保存在堆内存中),获取到该对象的锁之后才能执行同步代码块/同步方法。
注意:锁和同步代码并没有什么直接关系,可以直接创建一个对象作为锁对象——用于竞争获取,而同步代码可以和该锁可以没有关系。
下面2种写法的效果一致
public class T{
private int count = 10;
public synchronized void f1(){
count--;
System.out.println(Thread.currentThread().getName() + "count=" +count);
}
public void f2(){
synchronized(this){
count--;
System.out.println(Thread.currentThread().getName() + "count=" +count);
}
}
}
2、修饰静态方法——锁定该类的class字节码对象
下面两种写法的效果一样
public class T{
private static int count = 10;
public static synchronized void f1(){
count--;
System.out.println(Thread.currentThread().getName() + "count= " +count);
}
public static void f2(){
synchronized(T.class){
count--;
System.out.println(Thread.currentThread().getName() + "count= " +count);
}
}
}
性质
1、原子性和可见性
在没有对run()进行加锁(synchronized)的情况下,会出现重复的值——因为没有加锁,所以多个线程同时运行,出现的结果不唯一。
但是在加锁之后,一次只能有一个线程执行run()方法,所以输出的结果是9 8 7 6 5
Thread0count= 9
Thread3count= 8
Thread2count= 7
Thread1count= 6
Thread4count= 5
这里Thread的名字并不是按照0 1 2 3 4 这样排的,因为一次性创建了5个线程,但是这些线程谁获取到该对象的锁并且执行run()方法,
完全是由cpu调度来决定的,这个具有随机性。
public class T implements Runnable{
private int count=10;
public void run(){
count--;
System.out.println(Thread.currentThread().getName() + "count= " + count);
}
public static void main(String[] args) {
T t=new T();
for (int i=0;i<5;i++){
new Thread(t,"Thread"+i).start();
}
}
}
2、非同步方法和同步方法可以同时调用
同步方法是需要获取到该对象的锁,但是非同步方法并不需要获取该对象的锁,因而是没有影响的;可以同时调用
一个比喻:比如说同步方法就相当于是上厕所,需要获得厕所门的锁,但是非同步方法就好比是在厕所坑外面清洁的人,
并不需要获取厕所门的锁。
public class T {
public synchronized void f1(){
System.out.println(Thread.currentThread().getName() + "m1 start....");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m1 end .....");
}
public void f2(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "m2 end...." );
}
public static void main(String[] args) {
T5 t=new T5();
new Thread(()->t.f1(),"t1").start();
new Thread(()->t.f2(),"t2").start();
}
}
3、一个同步方法可以调用另外的一个同步方法(synchronized是可重入锁)
一个同步方法可以调用另外的一个同步方法,一个线程已经拥有了某个对象的锁,再次申请的时候仍然会获取该对象的锁
相当于锁定了2次 同一线程 同一把锁 所以能 也即:同一个线程可以多次锁定同一个对象
public class T {
public synchronized void f1(){
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
f2();
}
public synchronized void f2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
T t=new T();
new Thread(()->t.f1()).start();
}
}
4、子类的同步方法调用父类的同步方法
锁定的是同一个对象:子类的对象
class T extends T7{
@Override
synchronized void f1(){
System.out.println("child start");
super.f1();
System.out.println("child end");
}
}
public class T7 {
synchronized void f1(){
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2 end");
}
public static void main(String[] args) {
new T().f1();
}
}
5、synchronized在运行的过程中遇到异常是默认会释放锁
在处理并发的情况下,要额外的小心异常的处理,如果异常处理不好,会导致释放锁之后数据的不一致
例如:
在一个webAPP中,多个servlet线程访问同一块资源,如果某个servlet线程发生了异常,如果异常处理不合适,那么会导致数据有问题。
因为线程在执行的过程中遇到异常,自动释放锁之后,这个时候可能出现非原子性(遇到异常的servlet线程的操作并没有完成)。
spring有事务管理,就是在并发的情况下,如果遇到异常会自动回退到原始状态。
例子:
2个线程同时启动竞争对象锁,其中一个竞争成功后,对count一直进行++的操作,当count==5的时候,发生了异常,这个时候回默认
释放锁资源,然后另外一个线程就会获取到锁资源并且执行代码,但是!!!这个时候count是从5开始的,但是这个5是上一个线程
并未完成所有操作而产生的数据,因而该数据是有问题的!
public class T8 {
int count=0;
synchronized void f1(){
System.out.println(Thread.currentThread().getName() + " satrt");
while (true){
count++;
System.out.println(Thread.currentThread().getName() + "count= " +count );
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5){
int i=1/0;
}
}
}
public static void main(String[] args) {
T8 t8=new T8();
for (int i=0;i<2;i++){
new Thread(()->t8.f1()).start();
}
}
}
volatile
性质
可见性——使一个变量在线程之间可见
背景介绍:cpu和内存在运行速度上有很明显的差异(相差100倍左右),为了缓解运行速度上的矛盾,在cpu中设置一个缓存区,缓冲区存放着从主存(内存)中读取需要操作的数据,但是多线程的情况下(每个cpu都会有自己的缓存),当某一个cpu线程中对缓存数据的修改之后,其他的cpu缓存中并不会立刻更新数据(默认情况下,具体何时进行更新操作得看cpu什么时候空闲),所以如果多线程下其中一个线程对数据进行了修改,其他的线程并不会立刻更新数据,因而在数据上就会有问题(互相不可见);因而出现了volatile,volatile的作用就是:当某个cpu的缓存对数据进行修改之后,会立刻写到主存(内存)中,并且会通知其他的线程及时的更新数据,以获取最新的数据。
在把volatile注释掉的情况下,线程1不会结束(起码短时间内不会,可能在cpu空闲的时候会自动的去主存中更新数据)。
因为线程1中缓存的flag还是true,没有使用volatile及时更新缓存数据导致。
public class T {
private boolean flag=true;
void f1(){
System.out.println("m1 start....");
while (flag){
}
System.out.println("m1 end....");
}
public static void main(String[] args) {
T t = new T();
new Thread(()->t.f1()).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t9.flag = false;
}
}
volatile不具有原子性
volatile不具备原子性,并不能保证多个线程共同修改变量的时候所带来的不一致问题,因而不能替代synchronized
public class T {
private volatile int count=0;
public void m(){
for (int i=0;i<10000;i++) {
count++;
}
}
public static void main(String[] args) {
T t = new T();
List<Thread> threads = new ArrayList<>();
for (int i=0;i<10;i++){
threads.add(new Thread(t::m," thread "+i));
}
threads.forEach((o)->o.start());
threads.forEach((o->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
System.out.println(t10.count);
}
}
结果是小于10W的,假设线程1对count进行操作,加到了100之后刷新了主存,而线程2获取到了100之后,对count++操作到150之后提交,
但是线程3这个时候可能只++到101,然后提交,就会覆盖线程2的150
synchronized和volatile的区别
1、synchronized具有原子性和可见性,而volatile只有可见性
2、volatile仅能使用在变量级别,synchronized则可以使用在变量和方法上
3、volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
4、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。