文章
问答
冒泡
JAVA的多线程与高并发(二)

Synchronized 关键字

为什么需要锁

当多个线程去访问同一个资源的时候,需要对此资源上锁。


why:比如我们对一个数字做递增算法,如果由两个线程共同访问的时候,线程一读它为0,然后+1计算,在线程一内存里面还没有计算完成写回去的时候,线程二读它,还是为0,线程二+1再写回去。本来这是做了两次递增,结果应为2,但实际成了1.所以我们要对这个数字的访问上一把锁,就是说,一个线程再对这个数字访问的时候是独占此数字的,不允许别的线程来访问计算,只有线程一加完1并写回后,其他线程才可对数字继续进行调用。


以下代码,没有volatile或者synchronized的话,输出结果很小几率是我们想要的0:

package com.rongyu;

public class NoSync {
    public static void main(String[] args) {
        T t = new T();
        for (int i = 0; i < 100; i++) {
            new Thread(t,"Thread"+i).start();
        }

    }

    static class T implements Runnable {
        private /*volatile*/ int count = 100;

        public /*synchronized*/ void run() {
            count--;
            System.out.println(Thread.currentThread().getName() + "count =" + count);
        }
    }

}

加锁的几种方式

锁是锁定的某个对象,加锁的几种方法example:

package com.rongyu;

public class Sync {
    /**
     * synchronized关键字 对某个对象加锁
     */
    private int count = 0;
    private Object object = new Object();

    public void m() {
        synchronized (object) {
            count++;
            System.out.println(Thread.currentThread().getName() + "count =" + count);
        }
    }

    // 锁定当前对象方法一
    public void n() {
        //每次new一个对象太麻烦,可以使用this代替
        synchronized (this) {
            count++;
            System.out.println(Thread.currentThread().getName() + "count =" + count);
        }
    }

    // 锁定当前对象方法二
    public synchronized void b() {//等同于  synchronized (this)
        count++;
        System.out.println(Thread.currentThread().getName() + "count =" + count);
    }

}

注意,同步方法和非同步方式是可以同时调用的。因为非同步方法不需要拿锁。

不加锁导致的脏读

下面我们来看一个用所解决脏读问题的案例:


模拟银行账户,对业务的写方法加锁,对读方法不加锁,看会产生什么问题:

package com.rongyu;

import java.util.concurrent.TimeUnit;

public class Account {

    String name;
    double balance;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

// 写
    public synchronized void set(String name, double balance) {
        this.name = name;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }

    // 读
    public /*synchronized*/ double getBalance(String name){
        return this.balance;
    }

    public static void main(String[] args) {
        Account a = new Account();
        new Thread(()->a.set("zhangsan",100.0)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(a.getBalance("zhangsan"));

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(a.getBalance("zhangsan"));
    }
}

"C:\Program Files\Eclipse Adoptium\jdk-11.0.14.101-hotspot\bin\java.exe" "-javaagent:D:\ideaFamily\IntelliJ IDEA 2021.1.3\lib\idea_rt.jar=57039:D:\ideaFamily\IntelliJ IDEA 2021.1.3\bin" -Dfile.encoding=UTF-8 -classpath D:\workspace\thread-demo\out\production\thread-demo com.rongyu.Account
0.0
100.0

Process finished with exit code 0

因为getBalance方法没有加锁,所以说我不需要等线程中的set方法执行完就可以执行,而set在放入“zhangsan”后,睡了2s才放100, 第一次执行的时候100还未放入,所以读到的为0.这就是脏读。当我们程序允许脏读的时候可以不加锁,因为锁会影响代码效率。

锁的可重入性

重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

synchronized 和 ReentrantLock 都是可重入锁。


个人理解:如果一个同步方法调用另外一个同步方法,如果两个方法加了同一把锁,那么这个时候申请仍然可以得到该对象的锁。比如:方法m1和m2都是是synchronized修饰的,m1能不能调用m2.m1开始的时候线程拿到了这把锁,然后m1调用m2,如果说这个时候不允许任何线程再来拿这把锁,就会发生死锁。但是如果这个时候发现m2是同一个线程,那么就被允许,这种叫做可重入锁。


example:

package com.rongyu;

import java.util.concurrent.TimeUnit;

public class KeChongRu {
    public synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("m1 end");

    }

    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2 start");
    }

    public static void main(String[] args) {
        new KeChongRu().m1();
    }
}

输出结果:

m1 start
m2 start
m1 end

所谓的重入锁,就是拿到锁之后可以不停地对其加锁加锁,但锁定地还是同一个对象,去掉一个锁就减一个。减没有了,此对象就可被别的线程调用。

异常锁

程序在执行过程中,如果发生了异常,默认情况下锁会被释放掉。所以,在并发处理地过程中,有异常一定要注意,不然锁失效会导致结果不对的情况。

package com.rongyu;

import java.util.concurrent.TimeUnit;

public class AbnormalLock {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + "start");
        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;//此处会发生异常导致锁被释放,如果不想释放锁,可以及逆行catch让循环继续
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        AbnormalLock t = new AbnormalLock();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        };

        new Thread(r,"t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(r,"t2").start();
    }
}

输出结果可以看到,t1执行到5报错后,锁被释放,t2拿到继续执行。

t1start
t1count =1
t1count =2
t1count =3
t1count =4
t1count =5
t2start
t2count =6
Exception in thread "t1" java.lang.ArithmeticException: / by zero
	at com.rongyu.AbnormalLock.m(AbnormalLock.java:18)
	at com.rongyu.AbnormalLock$1.run(AbnormalLock.java:29)
	at java.base/java.lang.Thread.run(Thread.java:829)
t2count =7
t2count =8
t2count =9
t2count =10
    ...

锁升级

new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁

jdk1.5之前,sync是重量级锁,如果要使用sync的话,他首先会去操作系统去申请一把大锁,但是在之后,sync进行了锁优化,第一个访问这把锁的线程,它会在对象的markword中做一个标记,这就是偏向锁,如果有多个线程去访问资源的时候,他会将偏向锁撤销,换成轻量级锁,如果竞争加剧,那么他就会向操作系统去申请一把重量级锁。


1,锁的升级过程就是锁的竞争程度的结果


1,偏向锁:同步只有一个线程使用,使用过程中不存在竞争


2,轻量锁:第一个线程进入同步块后,第二个线程来竞争锁,此时会增加在调用栈记录mark word的锁信息(对象中mark指向其),如果由于第二个线程的竞争导致栈中的mark word更新失败,并且目前对象中的mark不指向当前线程栈中的mark就会升级为重量锁(第二个线程会修改栈中的记录,尝试修改成功),第二个线程不会阻塞会不断的尝试修改(一定时间内),自旋成功获取锁,此时还是自旋锁,自选失败升级为重量锁,阻塞


3,重量锁


synchronized的执行过程:

  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁

  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1

  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。

  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁

  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  6. 如果自旋成功则依然处于轻量级状态。

  7. 如果自旋失败,则升级为重量级锁。

上面几种锁都是JVM自己内部实现,当执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作。


在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们


偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁。


如果线程争用激烈,那么应该禁用偏向锁。

引用:https://blog.csdn.net/xitingcan/article/details/117516942

java

关于作者

BenbobaBigKing
获得点赞
文章被阅读