前言
为什么用synchronized?
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile(这个关键字也很重要,可以关注下)
为何使用同步?
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(增删改查),将会导致数据的不准确,相互之间产生冲突。类似于在atm取钱,银行数据确没有变,这是不行的,要存在于一个事务中。因此加入了同步锁,以避免在该线程没有结束前,调用其他线程。从而保证了变量的唯一性,准确性。
实现原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
(1)synchronized同步代码块:synchronized关键字经过编译之后,会在同步代码块前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令的时候,首先尝试获取对象的锁,如果这个锁没有被锁定或者当前线程已经拥有了那个对象的锁,锁的计数器就加1,在执行monitorexit指令时会将锁的计数器减1,当减为0的时候就释放锁。如果获取对象锁一直失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
(2)同步方法:方法级的同步是隐式的,无须通过字节码指令来控制,JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用的时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能执行方法,最后当方法执行完(无论是正常完成还是非正常完成)时释放monitor对象。在方法执行期间,执行线程持有了管程,其他线程都无法再次获取同一个管程。
怎么使用synchronized?
synchronized可以用于修饰方法、代码块(在方法体内的代码块),但是不能用于修饰类、构造器或者非方法体内的代码块,具体的例子如下:
1 使用synchronized修饰方法,同步方法默认用this或者当前类class对象作为锁。
//使用synchronized修饰
public synchronized void printStr1(){
System.out.println("PrintStr1 start...");
printInfo("str1",3000);
}
//不使用synchronized修饰
public void printStr3(){
System.out.println("PrintStr3 start");
printInfo("str3 ",1000);
}
//重用方法
public void printInfo(String info,long time){
System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
try {
Thread.sleep(time);
System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
}
public static void main(String[] args){
Account ac=new Account("2",1000.);
new Thread(ac::printStr1).start();
new Thread(ac::printStr3).start();
}
可以看到结果中printStr3由于没有加synchronized,虽然printStr1先取得了锁,但是由于printStr1中线程sleep了3秒,所以printStr3先进行执行了,即名字为 Thread-1,即没有添加synchronized的方法在多线程时,执行不会受到限制,因此是线程不安全的,容易使操作的数据出现脏读的情况。
2 两个方法都用 synchronized 修饰,但是不同线程作用于同一个对象
public synchronized void printStr(){
System.out.println("PrintStr start ...");
printInfo("str",1000);
}
public synchronized void printStr1(){
System.out.println("PrintStr1 start...");
printInfo("str1",3000);
}
public void printInfo(String info,long time){
System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
try {
Thread.sleep(time);
System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
}
public static void main(String[] args){
//只声明了一个对象
Account ac=new Account("2",1000.);
new Thread(ac::printStr).start();
new Thread(ac::printStr1).start();
}
可以从结果看出,不同的线程作用于同一个对象,则synchronized的锁是相同的,所以如果printStr先获得了线程,就会一直执行,知道释放锁,然后printStr1才开始执行
3 两个方法都用 synchronized 修饰,但是不同线程作用于不同对象,不同对象调用的方法不一样
public synchronized void printStr(){
System.out.println("PrintStr start ...");
printInfo("str",1000);
}
public synchronized void printStr1(){
System.out.println("PrintStr1 start...");
printInfo("str1",3000);
}
public void printInfo(String info,long time){
System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
try {
Thread.sleep(time);
System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
}
public static void main(String[] args){
Account ac=new Account("2",1000.);
Account ac1=new Account("2",1000.);
new Thread(ac::printStr).start();
new Thread(ac1::printStr1).start();
}
从这个结果可以看出,当两个线程作用域不同对象的时候,他们的锁也不一样,所以printStr和printStr1是一起执行的,其中printStr线程名称为 Thread-0, printStr1为线程Thread-1,由于printStr1 阻塞(sleep)时间为3秒,而printStr阻塞时间为1秒,所以Thread-0先执行结束释放锁,但是这个锁释放后就闲置了,没有被printStr1使用,因为两者属于不同的锁。
4 两个方法都用 synchronized 修饰,但是不同线程作用于不同对象,不同对象调用的方法一样
public synchronized void printStr(){
System.out.println("PrintStr start ...");
printInfo("str",1000);
}
public synchronized void printStr1(){
System.out.println("PrintStr1 start...");
printInfo("str1",3000);
}
public void printInfo(String info,long time){
System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
try {
Thread.sleep(time);
System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
}
public static void main(String[] args){
Account ac=new Account("2",1000.);
Account ac1=new Account("2",1000.);
new Thread(ac::printStr1).start();
new Thread(ac1::printStr1).start();
}
可以看到,不同对象由于具有不同的锁,所以当不同对象调用相同的方法时,会出现以下的形式:
【注】以上的情况,也说明当分布式情况下,synchronized就失效了,因为不同的对象调用相同的方法,最终是多个线程一起调用,会导致操作的数据出现问题。那么分布式情况下怎么保证线程安全呢,一般有三种方法:
1 队列:将所有要执行的任务放入队列中,然后一个一个消费,从而避免并发问题
2 悲观锁:将数据记录加版本号,如果版本号不一致就不更新,这种方式同Java的CAS理念类似。
3 分布式锁:
3.1 基于数据库实现的分布式锁
3.2 基于Zookeeper实现分布式锁
3.3 基于缓存(redis)来实现分布式锁
5 synchronized作用于静态方法,这个时候,使用synchronized修饰的方法在类加载器中只有一个,无论是几个不同的对象调用,只要某个对象正在调用此静态方法,且具备了方法锁,另一个线程只有等待。
public static synchronized void printStr2(){
System.out.println("PrintStr2 start ...");
System.out.println("==>"+Thread.currentThread().getName()+" create");
try {
Thread.sleep(1000);
System.out.println("==>"+Thread.currentThread().getName()+" execute");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==>"+Thread.currentThread().getName()+" end");
}
public void run() {
printStr2();
}
public static void main(String[] args){
Account ac=new Account("2",1000.);
Account ac1=new Account("2",1000.);
Thread th1=new Thread(ac);
Thread th2=new Thread(ac1);
th1.start();
th2.start();
}
6 当把synchronized删除后,会产生以下的结果,所以synchronized当静态条件下,其实充当了一个类锁,而不是对象锁,能保证同一个类的不同对象实例调用此方法时,如果被锁,则必须等待。但是这个并不是说在分布式系统中就可以用static synchronized 修饰的方法就是线程安全,因为static修饰只是在类加载器中存在唯一一份,但是分布式系统中,不同的主机其实都需要进行加载一份类,这个时候,其实不能满足要求,还是需要借助以上说的分布式系统中的解决方案来执行。
public static void printStr2(){
System.out.println("PrintStr2 start ...");
System.out.println("==>"+Thread.currentThread().getName()+" create");
try {
Thread.sleep(1000);
System.out.println("==>"+Thread.currentThread().getName()+" execute");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==>"+Thread.currentThread().getName()+" end");
}
public void run() {
printStr2();
}
public static void main(String[] args){
Account ac=new Account("2",1000.);
Account ac1=new Account("2",1000.);
Thread th1=new Thread(ac);
Thread th2=new Thread(ac1);
th1.start();
th2.start();
}
7 synchronized 作用于方法体中的代码块,作用是只针对代码中的同步部分进行修饰,避免了将整个方法都使用synchronized进行修饰,一定程度上可以减小程序开销,加快程序的执行效率,但问题就是我们需要找到同步代码部分,一般是整个方法体的一小部分。这里需要注意什么是同步代码,总体来讲,就是改变一些共享数据值的代码,比如完成银行取款时候的账户余额等。
public void printStr3(){
System.out.println("PrintStr3 start");
printInfo("str3 ",1000);
}
public void run() {
//同步条件:YourClass.class / this / 单例对象
synchronized(this){
printStr3();
}
}
public static void main(String[] args){
Account ac=new Account("2",1000.);
Account ac1=new Account("2",1000.);
Account ac2=new Account("2",1000.);
Account ac3=new Account("2",1000.);
//new Thread(ac::printStr1).start();
//new Thread(ac1::printStr1).start();
//new Thread().start();
Thread th1=new Thread(ac);
Thread th2=new Thread(ac1);
Thread th3=new Thread(ac2);
Thread th4=new Thread(ac3);
th1.start();
th2.start();
th3.start();
th4.start();
}
使用 this 的时候,主要是当前类对象的锁,则不同的对象,锁不一样,就导致执行仍然是乱序执行,可以将其改为 单例对象 和 类锁,具体操作如下:
8 将其改为 单例对象 和 类锁
类锁:
synchronized(Account.class){
printStr3();
}
单例对象锁:
private static final Account ac=new Account();
synchronized(ac){
printStr3();
}
【以上是synchronized的相关原理和使用,关于多线程还有很多内容,慢慢学习】