网上有些资料将使用ThreadLocal也定义为多线程同步的一种方式,但是多线程同步的前提是变量共享。
ThreadLocal并不是为了解决多线程间共享变量的问题。举个例子,在一个电商系统中,用一个Long型变量表示某个商品的库存量,多个线程需要访问库存量进行销售,并减去销售数量,以更新库存量。在这个场景中,是不能使用ThreadLocal类的。
ThreadLocal适用的场景是,多个线程都需要使用一个变量,但这个变量的值不需要在各个线程间共享,各个线程都只使用自己的这个变量的值。这样的场景下,可以使用ThreadLocal。此外,我们使用ThreadLocal还能解决一个参数过多的问题。例如一个线程内的某个方法f1有10个参数,而f1调用f2时,f2又有10个参数,这么多的参数传递十分繁琐。那么,我们可以使用ThreadLocal来减少参数的传递,用ThreadLocal定义全局变量,各个线程需要参数时,去全局变量去取就可以了。
ThreadLocal内部维护了一个静态内部类ThreadLocalMap,每个线程都有一个ThreadLocalMap,用来保存该线程关联的所有ThreadLocal类型的变量,因为一个线程中可以存在多个ThreadLocal实例,ThreadLocalMap中key为当前ThreadLocal实例,value
为当前ThreadLocal实例对应的值。
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,
副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。变量局部化
package com.xb.test;
public class ThreadLocalTest {
class Bank {
private ThreadLocal<Integer> account = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 100;
};
};
public int getAccount() {
return account.get();
}
/**
* 用同步方法实现
*
* @param money
*/
public void save(int money) {
account.set(account.get()+money);
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
Thread thread1 = new Thread(new_thread , "线程1");
thread1.start();
Thread thread2 = new Thread(new_thread , "线程2");
thread2.start();
}
public static void main(String[] args) {
ThreadLocalTest st = new ThreadLocalTest();
st.useThread();
}
}
关于线程同步(6种方式)
- 同步方法
- 同步代码块
- 使用重入锁实现线程同步(ReentrantLock)
- 使用特殊域变量(volatile)实现同步(每次重新计算,安全但并非一致)
- 使用原子变量实现线程同步(AtomicInteger(乐观锁))
- 使用阻塞队列实现线程同步(BlockingQueue (常用)add(),offer(),put()
1.同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
package com.xb.test;
public class SynchronousTest {
class Bank {
private int account = 100;
public int getAccount() {
return account;
}
/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
/**
*用同步代码块实现
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
Thread thread1 = new Thread(new_thread , "线程1");
thread1.start();
Thread thread2 = new Thread(new_thread , "线程2");
thread2.start();
}
public static void main(String[] args) {
SynchronousTest st = new SynchronousTest();
st.useThread();
}
}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
2.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){
}
代码示例如一所示
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3.使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力
ReentrantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
package com.xb.test;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import com.xb.test.AtomicTest.Bank;
import com.xb.test.AtomicTest.NewThread;
public class ReentrantLockTest {
class Bank {
private int account = 100;
private ReentrantLock lock = new ReentrantLock();
public int getAccount() {
return account;
}
/**
* 用同步方法实现
*
* @param money
*/
public void save(int money) {
lock.lock();
try {
account += money;
} finally {
lock.unlock();
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
Thread thread1 = new Thread(new_thread , "线程1");
thread1.start();
Thread thread2 = new Thread(new_thread , "线程2");
thread2.start();
}
public static void main(String[] args) {
ReentrantLockTest rlt = new ReentrantLockTest();
rlt.useThread();
}
}
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
4.使用特殊域变量(volatile)实现线程同步
常用于保持内存可见性和防止指令重排序。
内存可见性(Memory Visibility)是指所有线程都能看到共享内存的最新状态。
volatile关键字通过“内存屏障”
来防止指令被重排序。
参考链接:volatile关键字的作用、原理
package com.xb.test;
public class VolatileTest {
class Bank {
private volatile int account = 0;
public int getAccount() {
return account;
}
/**
* 用同步方法实现
*
* @param money
*/
public void save(int money) {
account += money;
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
Thread thread1 = new Thread(new_thread , "线程1");
thread1.start();
Thread thread2 = new Thread(new_thread , "线程2");
thread2.start();
}
public static void main(String[] args) {
VolatileTest st = new VolatileTest();
st.useThread();
}
}
6.使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成,跟数据库的事务的原子性有些类似。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。小工具包,支持在单个变量上解除锁的线程安全编程。
类摘要 | |
可以用原子方式更新的 boolean 值。 | |
可以用原子方式更新的 int 值。 | |
可以用原子方式更新其元素的 int 数组。 | |
基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。 | |
可以用原子方式更新的 long 值。 | |
可以用原子方式更新其元素的 long 数组。 | |
基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。 | |
AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。 | |
可以用原子方式更新的对象引用。 | |
可以用原子方式更新其元素的对象引用数组。 | |
基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。 | |
AtomicStampedReference 维护带有整数"标志"的对象引用,可以用原子方式对其进行更新。 |
其中AtomicInteger(乐观锁)为例 :
表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAndGet(int dalta) : 以原子方式将给定值与当前值相加
int | getAndAdd(int delta) 以原子方式将给定值与当前值相加。 |
int | getAndDecrement() 以原子方式将当前值减 1。 |
int | getAndIncrement() 以原子方式将当前值加 1。 |
int get() : 获取当前值
set():设置给定初始值
package com.xb.test;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTest {
class Bank {
private AtomicInteger account = new AtomicInteger(100);
public int getAccount() {
return account.get();
}
/**
* 用同步方法实现
*
* @param money
*/
public void save(int money) {
account.addAndGet(money);
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
Thread thread1 = new Thread(new_thread , "线程1");
thread1.start();
Thread thread2 = new Thread(new_thread , "线程2");
thread2.start();
}
public static void main(String[] args) {
AtomicTest st = new AtomicTest();
st.useThread();
}
}