Synchronized同步化的四种方法!

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

对于同步化,有几点需要声明以便理解:类是被声明的,对象是按照类的具体声明来决定怎样被创建.每个对象都是拥有一把锁的!在多线程同一对象进行操作的情况下,这把锁的获取权,将会被各个线程锁争夺,唯有获取了该对象的锁,才能对该对象进行操作!

假定现在有一个类:public class Account 代表账户

声明如下:

public class Account {

    private int balance;      //代表余额

    private String name;          //用户的姓名

    public int deposit (int count){   //入账,存入金额

         this.balance += count;

         return this.balance;

    }

    public int chargeoff(int count){  //出账,取出金额

         this.balance -= count;

         return this.balance;

    }

    //setter andgetter

    public int getBalance() {

         return balance;

    }

    public void setBalance(int balance) {

         this.balance = balance;

    }

    public StringgetName() {

         return name;

    }

    public void setName(Stringname) {

         this.name = name;

    }

}

 

对象(Object)的同步化:

例如:我们在其他的类中填写代码的时候要使用到Account这个类,来操作用户的账户.

假设这个类是public class Operation

public class Operation {

    public static void operate(count1, count2){

         Account a1 = new Account();

         Account a2 = new Account();

         synchronized(a1){

             a1.deposit(count1);

             a1.chargeoff(count2);

         }

         a2.deposit(count1);

         a2.chargeoff(count2);

    }  

}

如果想要使用Operation这个类,执行对两个账户a1,a2的修改,那么锁住对象变得很关键了.

在实际环境中,若果对a1或者a2两个变量在一小段时间内,同时多线程操作,那么thread-1和thread-2获取的balance很可能是一样的,这样一来,在thread-1还没有操作完毕之后,thread-2所读取的balance是thread-1操作到一半或者是还没有操作的数值.这样的话,会产生”脏数据”.对此,我们给a1这样的对象加上了锁,线程必须取得这样的一把唯一的锁才能对该对象进行操作,让他不像a2那样有着被多条(两条以上)的线程同时操作,避免了脏数据的产生.

       值得注意的是:在今天的web项目开发中,用的更多的是分布式开发形式:

用户的数据一般都是被持久化地放在了数据库中(比如MySQL),由于是分布式开发,数据库不止在一台机器上,那么这样的分布式项目所要面临的是数据库之间的共享资源不一致的情况.

       因此,为了解决这个问题,我们就必须引入「分布式锁」.这个之后再说.

类中方法(Method)的同步化

例如:

       public synchronized int deposit (int count){   //入账,存入金额

         this.balance += count;

         return this.balance;

    }

    public synchronized int chargeoff(int count){  //出账,取出金额

         this.balance -= count;

         return this.balance;

    }

这样的方法被同步化了以后,当他的所属类的实例化对象在使用这个同步化方法的时候,该实例化对象的锁将会被线程锁争夺,夺取了锁才能获得方法的执行权.

代码块(Code Block)的同步化:

synchronized(this.balance){

       this.balance += count;

       this.balance -= count;

  }

一次只有一个线程进入并执行该代码块,锁依旧属于对象,是含有这个代码块的对象.

类(class)的同步化:

涵盖以上三种同步化,是同步化的高达范围.

只要是按照该类创建出来的对象,都会自动地带上锁,以便线程的操作.

Java线程池

Java通过java.util.concurrent.Executors提供四种线程池,分别为:
newCachedThreadPool缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。一般线程的个数是cpu核数的一倍或者二倍.如果i7-8750H是六核的,那么可以设为12或者6.
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

大部分GUI程序都是单线程的,甚至很多大型游戏都是单线程的,这就导致了硬件的一部分浪费,一核工作多核围观的情况!很多都没有多线程优化.

ExecutorService executorService1= Executors.newFixedThreadPool(12);

//十二个线程:Intel Core i7-8750H cpu核数的二倍
   executorService1.submit(new Runnable() {       //线程执行器 提交任务
      
@Override
      
public void run() {
         //要执行的方法
      
}
   });

ExecutorService executorService2 = Executors.newScheduledThreadPool(6);
((ScheduledExecutorService) executorService2).schedule(new Runnable() {
   @Override              
   
public void run() {
       //要执行的方法
   }
},100,TimeUnit.SECONDS);//意味着10秒执行一次 ExecutorService executorService3 = Executors.newCachedThreadPool();
executorService3.execute(new Runnable() {
   @Override
   
public void run() {
       //要执行的方法
   
}
}); ExecutorService executorService4 = Executors.newSingleThreadExecutor();
executorService4.submit(new Runnable() {
   @Override
   
public void run() {
       //要执行的方法
   }
});  该类各部分的关系如下:public interface ExecutorService extends Executorpublic interface Executor public static ExecutorServicenewFixedThreadPool(int nThreadspublic static ExecutorService newSingleThreadExecutor()public static ExecutorServicenewCachedThreadPool()public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize

 

分布式锁的实现方式(部分转载自51CTO)

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。 

JavaWeb关于高并发,线程,同步化,堆与栈的各类问题 随笔 第1张

如图,假设同一个账户,用户A和用户B都要去进行取款操作,账户的原始余额是2000,用户A要去取1500,用户B要去取1000,如果没有锁机制的话,在并发的情况下,可能会出现余额同时被扣1500和1000,导致最终余额的不正确甚至是负数。但如果这里用到乐观锁机制,当两个用户去数据库中读取余额的时候,除了读取到2000余额以外,还读取了当前的版本号version=1,等用户A或用户B去修改数据库余额的时候,无论谁先操作,都会将版本号加1,即version=2,那么另外一个用户去更新的时候就发现版本号不对,已经变成2了,不是当初读出来时候的1,那么本次更新失败,就得重新去读取最新的数据库余额。

通过上面这个例子可以看出来,使用「乐观锁」机制,必须得满足:

(1)锁服务要有递增的版本号version

(2)每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

悲观锁也叫作排它锁,在Mysql中是基于 for update 来实现加锁的:

 

 

共享锁(lock in share mode)和排他(for update)锁:

 

 JavaWeb关于高并发,线程,同步化,堆与栈的各类问题 随笔 第2张

 

JavaWeb关于高并发,线程,同步化,堆与栈的各类问题 随笔 第3张

JavaWeb关于高并发,线程,同步化,堆与栈的各类问题 随笔 第4张

u_user:

 

 

没什么好讲的,在Navicat中开2个窗口尝试就知道了;

窗口1:

BEGIN;

select *from u_user where id = 1 for UPDATE;

select *from u_user where id = 1 lock in share mode;

COMMIT;

窗口2:

select *from u_user where id = 1 for UPDATE;

select *from u_user where id = 1 lock in share mode;

select *from u_user where id = 1 ;

UPDATEu_user set address="bjpowernode " where id =1 ;

# isnertudpate delete 自动加排他锁 for update

UPDATEu_user set address="bjpowernode " where id =1 for update;

UPDATEu_user set address="bjpowernode " where id =1 lock in share mode;

 

小小的总结:共享锁(lockin share mode)在事务开启的情况下,其他窗口的加了共享锁的语句会遭遇堵塞.一定要等之前的线程执行完毕之后才能轮到下一次查询.每一个窗口相当于一个线程,这是数据库自己的线程.

 

Update ,insert , delete 三种语句不可以再后面加 for update或者lockin share mode,否则客户端会提示语法错误.三者在sql内,是默认加锁的.

事务没有完结,等待的线程就会进入堵塞状态.

Commit之后,update的执行就开始了.

 

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄