一、什么是线程的同步
线程有自己的私有数据,比如栈和寄存器,同时与其它线程共享相同的虚拟内存和全局变量等资源。 在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是当多个线程同时读写同一份共享资源的时候,会引起冲突,例如在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。这时候就需要引入线程同步机制使各个线程排队一个一个的对共享资源进行操作,而不是同时进行。
简单的说就是,在多线程编程里面,一些数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
二 实现线程同步的方式
通过synchronized关键字和lock锁
1.synchronized关键字
synchronized关键字synchronized修饰色方法称为同步方法,也可以修饰代码,称为同步代码块
同步方法:可以是静态的也可以是非静态的,但不能是抽象类的抽象方法
即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
public synchronized void aMethod() { // do something } public static synchronized void anotherMethod() { // do something }
同步代码块:
即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
synchronized(object){ }
案例:买票
不安全的买票
package com.example.demo.therad.synchronizedThread;
//买票
public class BuyTicket implements Runnable{
private int ticketNum=10;//票数
Boolean flag=true;//线程外部结束标志
public void run() {
while(flag) {
buy();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//买票方法
public void buy() {
if (ticketNum<=0) {
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第:"+ticketNum-- +"张票");
}
}
测试:
public static void main(String[] args) throws Exception{
BuyTicket buyTicket=new BuyTicket();
Thread t1=new Thread(buyTicket,"学生");
Thread t2=new Thread(buyTicket,"老师");
Thread t3=new Thread(buyTicket,"黄牛党");
t1.start();
t2.start();
t3.start();
}
结果:
黄牛党买到了第:10张票
学生买到了第:9张票
老师买到了第:8张票
学生买到了第:7张票
老师买到了第:7张票
黄牛党买到了第:7张票
学生买到了第:6张票
老师买到了第:4张票
黄牛党买到了第:5张票
黄牛党买到了第:3张票
老师买到了第:3张票
学生买到了第:2张票
老师买到了第:1张票
可以看到 学生、老师、黄牛党 有买到重复的票。
安全案例:
用synchronized修饰方法
package com.example.demo.therad.synchronizedThread;
//买票
public class BuyTicket implements Runnable{
private int ticketNum=10;//票数
Boolean flag=true;//线程外部结束标志
public void run() {
while(flag) {
buy();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//买票方法
public synchronized void buy() {
if (ticketNum<=0) {
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第:"+ticketNum-- +"张票");
}
}
测试结果
学生买到了第:10张票
老师买到了第:9张票
黄牛党买到了第:8张票
老师买到了第:7张票
黄牛党买到了第:6张票
学生买到了第:5张票
学生买到了第:4张票
黄牛党买到了第:3张票
老师买到了第:2张票
老师买到了第:1张票
银行存钱和取钱案例:
不安全案例
创建一个账户
package com.example.demo.therad.synchronizedThread;
//账户
public class Xccount {
//账户中的钱
public int money;
public Xccount(int money) {
this.money=money;
}
}
取钱方法
package com.example.demo.therad.synchronizedThread;
//银行
public class Bank implements Runnable{
Xcount xcount;
private int drowMoney;//取金额
public Bank(Xcount xcount,int drowMoney) {
this.drowMoney=drowMoney;
this.xcount=xcount;
}
public void run() {
drow();
}
//取钱方法
public void drow() {
if (xcount.money-drowMoney<0) {
System.out.println(Thread.currentThread().getName()+"余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
xcount.money=xcount.money-drowMoney;
System.out.println(Thread.currentThread().getName()+":取款:"+drowMoney);
System.out.println(Thread.currentThread().getName()+":账户余额:"+xcount.money);
}
}
测试
public static void main(String[] args) throws Exception{
Xcount xcount=new Xcount(100);
Bank bank=new Bank(xcount, 50);
Bank bank1=new Bank(xcount, 100);
Thread t1=new Thread(bank,"小红");
Thread t2=new Thread(bank1,"小绿");
t1.start();
t2.start();
}
结果:
小红:取款:50
小绿:取款:100
小红:账户余额:-50
小绿:账户余额:-50
明显取钱结果是有问题的
安全取钱
因取钱共同操作的 Xccount 账户 对Xccount加锁
package com.example.demo.therad.synchronizedThread;
//银行
public class Bank implements Runnable{
Xccount xcount;
private int drowMoney;//取金额
public Bank(Xccount xcount,int drowMoney) {
this.drowMoney=drowMoney;
this.xcount=xcount;
}
public void run() {
drow();
}
//取钱方法
public void drow() {
synchronized (xcount) {
if (xcount.money-drowMoney<0) {
System.out.println(Thread.currentThread().getName()+"余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
xcount.money=xcount.money-drowMoney;
System.out.println(Thread.currentThread().getName()+":取款:"+drowMoney);
System.out.println(Thread.currentThread().getName()+":账户余额:"+xcount.money);
}
}
}
结果
小红:取款:50
小红:账户余额:50
小绿余额不足
同步方法:线程在执行同步方法时是具有排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。
同步块:同步块是通过锁定一个指定的对象,来对同步块中包含的代码进行同步;
如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。
lock锁
通过显式定义锁实现同步。
java.util.concurrent,locks.lock接口是控制多个线程对共享资源进行访问的工具。锁可以对共享资源的独占访问,每次只能有一个线程对lock对象加锁,线程开始访问共享资源前先获得lock对象。
ReentrantLock类实现了Lock,拥有于synchronized相同的并发性和内存语义,比较常用。
案例用Lock锁 安全买票
package com.example.demo.therad.synchronizedThread;
//买票
import java.util.concurrent.locks.ReentrantLock;
public class BuyTicket implements Runnable{
private int ticketNum=20;//票数
Boolean flag=true;//线程外部结束标志
private final ReentrantLock lock=new ReentrantLock();
public void run() {
while(flag) {
try {
Thread.sleep(1000);
lock.lock();//加锁
if (ticketNum<=0) {
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+
"买到了第:"+ticketNum-- +"张票");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();//解锁
}
}
}
}
结果
黄牛党买到了第:20张票
老师买到了第:19张票
学生买到了第:18张票
老师买到了第:17张票
黄牛党买到了第:16张票
学生买到了第:15张票
老师买到了第:14张票
黄牛党买到了第:13张票
学生买到了第:12张票
学生买到了第:11张票
老师买到了第:10张票
黄牛党买到了第:9张票
黄牛党买到了第:8张票
学生买到了第:7张票
老师买到了第:6张票
老师买到了第:5张票
学生买到了第:4张票
黄牛党买到了第:3张票
老师买到了第:2张票
黄牛党买到了第:1张票
区别:
来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;(注意:如果使用lock.lock()方法获取锁,线程也是不能被中断的,使用lock.trylock()获取锁,线程可以被中断)
是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,
1、synchronized和lock的用法区别
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
2、synchronized和lock性能区别
synchronized是托管给JVM执行的,
而lock是java写的控制锁的代码。
在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
2种机制的具体区别:
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
3、synchronized和lock用途区别
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候
下面细细道来……
先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);
第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
lock还与synchronized的不同,是可以实现公平锁和非公平锁的
public ReentrantLock(boolean fair)
当fair为true的时候,是公平锁
false的时候为非公平锁
如果是公平锁,则会按照队列的顺序进行执行
如果是非公平锁,则会按优先级高的先执行