多线程与高并发
- synchronized 当多个线程同时访问同一个资源的时候需要对这个资源上锁。
- synchronized 既保证原子性,也保证线程间的可见性
当我们对一个数字进行递增操作时,如果两个程序同时访问,第一个线程读到count=0,并对其+1,在自己线程内部的内存里还没有写回去的时候;第二个线程读到的count也是0,并+1写回去;但是程序明明对count进行了两次+1操作,但结果还是1。
那么我们对这个递增过程加上一把锁,当第一个程序访问的时候,这个资源是它独占的,不允许别的线程访问计算,当第一个线程计算完成并释放锁之后其它线程才能访问,这样就保证了线程安全。
public class Thread_006 {
private static int count = 0;
public static void main(String[] args) {
new Thread(()->{
countAdd();
}).start();
new Thread(()->{
countAdd();
}).start();
}
//去掉synchronized 可以看出来不加锁的情况下我们预期的结果与实际结果是不符合的
static /*synchronized*/ void countAdd(){
for (int i = 0; i < 100; i++) {
try {
//相当于你线程处理的业务
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
System.out.println(Thread.currentThread().getName()+"count = "+ count);
}
}
当不在countAdd方法上加锁时的执行结果:
Thread-1count = 155
Thread-0count = 155
当在countAdd方法上加锁的执行结果:
Thread-0count = 100
Thread-1count = 200
synchronized 加锁的方式
- 我们加锁代码有个原则:尽量少的锁代码,比如我的方法中只需要对count++进行synchronized,我们没必要把整个方法锁住,只锁count++就可以了。但是如果我们一个方法内有N多个地方需要加锁,我们就直接锁方法,就不要每次都让线程竞争了。
public class Thread_007 {
private int count = 0;
private Object o = new Object();//不可以使用String常量 Integer Long
//锁定Object
public void m1(){
synchronized (o){
count++;
}
}
//锁定当前对象
public void m2(){
synchronized (this){
count++;
}
}
//等同与m2锁定当前对象
public synchronized void m3(){
count++;
}
}
静态方法加锁
- 静态方法是没有this对象的,我们也可以不用new出来一个对象进行加锁
public class Thread_008 {
private static int count = 10;
//等同于synchronized (Thread_008.class)
public synchronized static void m1(){
count--;
}
public static void m2(){
synchronized (Thread_008.class){
count--;
}
}
}
synchronized保证原子性与线程间可见
public class Thread_009 {
private int count = 100000;
public /*synchronized*/ void run(){
count--;
System.out.println(Thread.currentThread().getName()+" count = "+count);
}
public static void main(String[] args) {
Thread_009 t = new Thread_009();
for (int i = 100000; i > 0 ; i--) {
new Thread(t::run,"Thread "+i+" ").start();
}
}
}
不加sychronized执行结果,我们可以看到多个线程输出了同一个值,最终结果不是我们预期的0
......
Thread 38223 count = 38230
Thread 38166 count = 38229
Thread 38226 count = 38229
Thread 38169 count = 38229
Thread 38284 count = 38230
Thread 38168 count = 38228
......
当我们加上sychronized时,会解决这个问题
Thread 4 count = 3
Thread 5 count = 2
Thread 2 count = 1
Thread 1 count = 0
面试题:模拟银行账户,对业务写方法加锁,对业务读方法不加锁,可以吗?
答:不可以,容易产生脏读现象;具体看代码,解决方法就是把读方法也加锁
public class Thread_010 {
String name;
double 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(){
return this.balance;
}
public static void main(String[] args) {
Thread_010 account = new Thread_010();
new Thread(()->account.set("柯南",300.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前账户余额:"+account.getBalance());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前账户余额:"+account.getBalance());
}
}
不加锁执行结果:
当前账户余额:0.0
当前账户余额:300.0
加锁执行结果:
当前账户余额:300.0
当前账户余额:300.0
synchronized 锁重入|异常锁
- 锁重入:一个同步方法内可以调用另一个同步方法,及一个类中两个方法都加了锁,那么他们锁定的都是同一个对象,当我们在m1中调用m2时,会发现他们呢是同一个线程的申请这把锁,允许执行m2,这就叫锁重入。
- 异常锁:程序执行过程中,如果某个环节出现异常,默认锁会被释放,外部等待的程序就会冲进来,程序乱入,可能会访问到异常时产生的数据;一般用try-catch解决,保证流程是不会被异常中断的。
synchronized 锁升级
- 这里强烈安利 《我是厕所所长一、二》-马士兵 马老师的文章生动的讲解了锁升级的一个过程,通俗易懂!
面试题:CAS(自旋锁)一定比系统锁的效率高吗?
答:不一定,分具体情况:执行时间短(加锁的代码),线程数少,用自旋;执行时间长,线程数多,用系统锁。