线程同步的实现
线程同步的方式多种多样,下面我们一起来学习几种简单的实现方式,在这之前我们先写一个Bank类模拟银行存钱的操作,账户初始余额为100,每次存钱操作都会存入相对应的金额。
public class Bank{
private int account = 100;
public void deposit(int money) {
account += money;
}
public int getAccount() {
return account;
}
}
再写一个线程实现了Runnable,它的run()方法会循环往账户里面存10块钱,循环1000次,存入金额10000元。先来看看非同步的情况下会发生什么,最后的结果往往会小于20100(因为简单的多线程发生错误的概率很低,所以在这里我通过大量的循环操作来增加出错的次数)。
public class Transfer implements Runnable{
private Bank bank;
private Transfer(Bank bank) {
super();
this.bank = bank;
}
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 1000; i++) {
bank.deposit(10);
}
}
public static void main(String[] args) {
Bank bank = new Bank();
Transfer t1 = new Transfer(bank);
Transfer t2 = new Transfer(bank);
new Thread(t1).start();
new Thread(t2).start();
try {
Thread.sleep(5000);
//延时五秒之后输出结果
System.out.println(bank.account);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
方法一:使用方法实现线程同步
所谓同步方法,就是用synchronized关键字修饰的方法,每个java对象都有一个内置锁,当用synchronized修饰方法时,内置锁就会保护整个方法。注意:synchronized也可以用来修饰静态方法,此时调用该静态方法会锁住整个类。这个方法的实现很简单,只要修改Bank类的deposit()方法为:
public synchronized void deposit(int money) {
account += money;
}
方法二:使用代码块实现线程同步
修改Bank类的deposit()方法为:
public void deposit(int money) {
synchronized (this) {
account += money;
}
}
方法三:使用重入锁实现线程同步
所谓重入锁,指的是以线程为单位,当一个线程获得对象锁之后,这个线程可以再次获取该对象锁,在本文,重入锁指的是ReentrantLock类,重新修改Bank类,其具体实现如下:public class Bank{
private int account = 100;
//创建重入锁实例
private Lock lock = new ReentrantLock();
public void deposit(int money) {
//打开锁
lock.lock();
try {
account += money;
} finally {
//关闭锁
lock.unlock();
}}
}
值得注意的是,
使用完毕后一定要记得unlock()!!!否则会造成死锁,正常我们使用可以把它放到finally块中。
方法四:使用线程局部变量实现线程同步
使用ThreadLocal来管理变量,每个使用该变量的线程都会获得该变量的副本,副本之间相互独立,互不影响。常用的方法有get(),initialValue(),set(T value)等等。重新修改Bank类,具体实现如下:
public class Bank{
//创建ThreadLocal变量
private ThreadLocal<Integer> account = new ThreadLocal<Integer>() {
@Override
//变量初始化
protected Integer initialValue() {
// TODO Auto-generated method stub
return 100;
}
};
public void deposit(int money) {
//设置变量值
account.set(account.get()+money);;
}
//获取变量值
public int getAccount() {
return account.get();
}
}
这时候在Transfer中两个线程account的值都是10100,想要获得正确的结果20100,需要进一步的处理。
方法五:使用阻塞队列实现线程同步
LinkedBlockingQueue<E>是一个FIFO的排序队列,其常用的方法有put(E e),向队尾增加一个元素,如果队列满则阻塞;size(),返回队列中的元素个数;take(),移出并返回队头元素,如果队列空则阻塞。编写一个Producer和Consumer类来实现。
private int size = 10;
private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
private class Producer implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i <size; i++) {
int b = new Random().nextInt(100);
try {
queue.put(b);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private class Consumer implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
int b;
for (int i = 0; i < size; i++) {
try {
b = queue.take();
System.out.println(b);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
方法六:使用信号量实现线程同步
信号量是用于在进程中发信号的整数值,Semaphore是一个计数信号量的实现类,信号量维护了一个许可集,通过aquire()&release()来获得和释放许可,其中许可数为1的信号量对于线程同步很有用。修改Transfer类,具体实现如下:
public class Transfer implements Runnable{
private Bank bank;
private Semaphore semaphore;
public Transfer(Bank bank, Semaphore semaphore) {
super();
this.bank = bank;
this.semaphore = semaphore;
}
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 1000; i++) {
try {
semaphore.acquire();
bank.deposit(10);
System.out.println(bank.getAccount());
semaphore.release();
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
方法七:使用原子变量实现线程同步
正因为绝大多数方法和操作都是非原子性的,才需要线程同步,如果我们能够把一系列的操作设置成原子性的,也就实现了同步。所谓原子性,即一系列的操作是一个整体,大家可以参考事务的概念,这一系列的操作要么不开始,一旦开始就直到执行结束。修改Bank类,具体实现如下:
public class Bank{
// private int account = 100;
private AtomicInteger account = new AtomicInteger(100);//初始化
public void deposit(int money) {
account.addAndGet(money);//addAndGet是原子操作
}
public int getAccount() {
return account.get();
}
}
总共总结了七种实现方法,然而还有更多的方法在学习中。
Q&A
Q:为什么要实现同步?
A:没有同步的线程在实际生活中将会产生不可预计的后果,想象一下,如果转帐的行为不是原子性的,一个账户的增加和另一个账户的减少不能同步的话,破产的不是你就是银行。
Q:创建多个线程可以充分利用系统的资源,然而线程越多,线程之间竞争资源和切换也会增大开销,如何把握这个度?
A:对于有大量短生命周期线程的情况,我们可以通过使用线程池的方式来优化多线程。其原理就是,线程池中存在着多个处于可运行状态的线程,当向线程池中添加Runnable实现类实例的时候,其实是将任务提交到线程池本身的线程中执行。任务执行完毕之后,线程并不会终止,而是继续处于可运行的状态等待新的线程任务。