共享模型
线程安全问题的Java体现
/**
* 线程安全问题的Java体现
*/
//如果两个线程做自增自减500次,那结果会是0吗
static class JavaThreadSecurity{
static int num = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
num++;
}
},"t1");
Thread thread2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
num--;
}
},"t2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(num);
}
}
运行结果:2023
问题分析
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而i--也同样
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
像计算这种工作都是在线程内部运行,计算完后线程将结果写回内存内,此时的num可能已经被另外一个线程给修改过了值,造成新写入的值与之前上下文不符合。造成了脏读。
临界区 Critical Section
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
synchronized 解决方案
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
利用synchronize解决基本的多线程操作变量问题
static Object room = new Object();
static int count = 0;
/**
* Synchronized的基本使用方法
*/
public static void testSynchronized() throws InterruptedException {
Thread thread1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (room){
count++;
}
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (room){
count--;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
结果无论怎么运行都会是0
原理分析
- synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
- 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),
- 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
思考
- 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性 代码会在运行完for循环后释放锁
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象 由于锁的不同,所以还是会造成线程安全问题
- 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象 由于t2没有上锁,所以还是会造成线程安全问题
利用面向对象的方式改造代码
public static void testSynchronized2() throws InterruptedException {
Room room = new Room();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (room){
room.increment();
}
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (room){
room.decrement();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(room.getCount());
}
/**
* 利用面向对象的思想对上一段代码进行改造
*/
static class Room{
public int getCount() {
//为保证读取到的值是正确的,所以也要加锁
synchronized (this){
return count;
}
}
private int count = 0;
public void increment(){
synchronized (this){
count++;
}
}
public void decrement(){
synchronized (this){
count--;
}
}
}
在这里room对象里的锁是使用自己为锁
方法上添加synchronize
线程八锁(部分)
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
这是一个特殊的锁,由于a方法添加了static关键字。所以实际上锁住的是Number类对象,而相反b对象锁住的只是n1。导致执行是没有占据同一个锁,这段代码执行的结果显而易见的是: 2 1s后 1
变量线程安全分析
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
public static void test1() {
int i = 10; i++;
}
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
//这里是局部变量的i++操作,实际上和静态变量的i++有些许不同。局部变量的i++只有这一条操作,具有原子性。
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
多线程操作test1方法,实际上在JVM里,为每一个线程都创造了独立的栈内存。而test1被创造一块栈帧后,局部变量i也会被创建一份,多线程里这些i都是相互独立的,是一块私有的内存,并不是共享的。所以并不存在线程安全问题。
而局部变量如果引用的是对象则稍有不同
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < 2; i++) {
//在这里创建两个线程去操作ThreadUnsafe类
new Thread(()->{
test.method1(200);
},"Thread"+(i+1)).start();
//最后结果是出现了异常
//Exception in thread "Thread2" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
}
}
static class ThreadUnsafe{
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber){
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method3() {
list.remove(0);
}
private void method2() {
list.add("1");
}
}
为什么会报错呢?
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
实际上演变成了多个线程对共享资源list的操作
而改成局部变量后程序可以正常运行了。原理和之前说的差不多。list不再是共享资源
public class Node03 {
public static void main(String[] args) {
Threadsafe test = new ThreadSafeSubClass();
for (int i = 0; i < 2; i++) {
//在这里创建两个线程去操作ThreadUnsafe类
new Thread(()->{
test.method1(200);
},"Thread"+(i+1)).start();
//最后结果是出现了异常
//Exception in thread "Thread2" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
}
}
}
class Threadsafe{
public void method1(int loopNumber){
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
//System.out.println(Thread.currentThread().getName()+"运行正常:"+i);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends Threadsafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(()->{
//在调用method2方法时,由于传参传进来的时其它线程传来的list,所以不会影响到method1中创建的list
//但我们创建了一个子类,这个子类里我们重写了method3方法。
//我们知道子类和父类可以共享资源,method3重启了一个新的线程来执行remove操作,这就会出现问题了。
//我们假设Thread1调用method3时,内部又有一个新的线程访问到了list对象。意味着这个list和新线程的list是一个共享资源。
list.remove(0);
}).start();
}
}
方法的访问修饰符也是可以一定程度上保护线程安全的。他限制了子类不能够随意覆盖父类的方法。因此就会有一些因为继承产生的线程安全问题。
常见的线程安全类
- String
- Integer (代指包装类)
- StringBuffffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,见后面分析
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
这种是线程安全的吗
虽然get和put方法在hashtable里都是线程安全的,但是组合在一起用就不是了。他只能保证get和put方法内的的内容是原子的。而其中的逻辑代码则不是。
例如接下来这张图。线程1判断get是不是null,但是还没有执行put的时候上下文切换了。线程2在判断完后put了key,此时又发生上下文切换,线程1put了key。这时候就发生了线程不安全了。
不可变类线程安全性
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//在这里其实subString方法没有对原对象进行修改操作,而是返回了一个newString,返回了一个新对象,所以说事线程安全的
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
实例分析
例1:
public class MyServlet extends HttpServlet {
// 是否安全?
Map<String,Object> map = new HashMap<>();
// 是否安全?
String S1 = "...";
// 是否安全?
final String S2 = "...";
// 是否安全?
Date D1 = new Date();
// 是否安全?
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
- map不是线程安全的,因为hashmap不是线程安全的。
- S1属于不可变类,所以他是线程安全的
- final修饰过的S2自然也是线程安全的。
- D1不是线程安全的,Date并不是线程安全类。
- D2不是线程安全的,因为D2只是D2这个引用值固定了不能变,但是其他的属性值还是可以变的,所以他还是线程不安全的。
例2:
public class MyServlet extends HttpServlet {
// 是否安全?
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}
userService是不是线程安全的呢?不是。userService只是MyServerlet的一个成员变量,具有共享属性。
count也不是线程安全的。在调用userService时count也同样会被共享。
例3:
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
那这段代码有没有线程安全问题呢?
答案是有的。
因为Spring里某一个对象都是单例模式,只要是单例模式就会被共享,里面的成员变量也不例外。所以他不是线程安全的。
解决方法就是用环绕通知中的局部变量,这样就保证了线程安全。
例4:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("", "", "")) {
// ...
} catch (Exception e) {
// ...
}
}
}
这个从底向上看。
首先DAOImpl是没有成员变量的,可以判断他们是线程安全的。因为他没有共享属性。connection也同样,不同的线程回创造不同的connection对象。
那Service是不是线程安全的。是的。虽然他内部有成员变量,但他内部已经没有可以修改的对象了,所以他是线程安全的。这有点类似先前说的不可变类型,都是无状态的。
Serverlet里使用的Service是不是线程安全的?是的。因为Service里虽然有成员变量,但是这个成员变量是私有的,也没有可以修改的属性,所以是线程安全的。
例5:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("", "", "");
// ...
conn.close();
}
}
只看UserDaoImpl
在这里Connection已经是一个局部变量了,肯定是会被多个线程共享的。所以他是线程不安全的。
例6:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection =null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("", "", "");
// ...
conn.close();
}
}
这段代码的不同与例5的点在于,Service实现类里线程调用update方法时,都会新建一个Dao对象,而不是当成一个实现类的成员变量。这样就不会有线程安全问题了。
因为线程一调用update时,每次都是一个新的DAO对象,这个DAO对象里的Connection每次都是一个NULL的状态,独立与之前创建的Connection对象。所以不会有线程安全问题。
例7:
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
所以面对这种不可预料的情况,把可能会出现线程安全的方法设置成final显得很有必要了。
习题:卖票
public class Node03 {
static Random random = new Random();
public static int RandomAmount(){return random.nextInt(5)+1;}
public static void main(String[] args) throws InterruptedException {
//用这个统计卖出去的票
List<Integer> list = new Vector<>();
//放所有的线程来调用join方法
List<Thread> threads = new Vector<>();
TicketWindow ticketWindow = new TicketWindow(1000);
for (int i = 0; i < 300; i++) {
Thread thread = new Thread(()->{
int amount = ticketWindow.sell(RandomAmount());
list.add(amount);
},"旅客"+(i+1));
threads.add(thread);
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("余票:"+ticketWindow.getCount());
System.out.println("卖出去的票数:"+list.stream().mapToInt(i -> i).sum());
}
}
class TicketWindow{
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public int sell(int amount){
if (this.count >= amount){
this.count -= amount;
System.out.println(Thread.currentThread().getName()+"买了"+amount+"张票成功!余票:"+this.count+"张");
return amount;
}else {
return 0;
}
}
}
其实有点难测出来。。但确实出现了卖出1000多张票的情况。
这段代码显然是线程不安全的,对于共享变量count没有加锁,在多线程的操作下肯定是会出现线程安全问题。哪怕Vector是线程安全的,但在多线程没有保护临界区的情况下,依旧会存爱线程安全的问题。
所以最好给sell方法加上synchronize关键字,这样就可以解决线程安全问题了。
习题:转账
public class Node03 {
static Random random = new Random();
public static int RandomAmount(){return random.nextInt(5)+1;}
public static void main(String[] args) throws InterruptedException {
Account account1 = new Account(2000);
Account account2 = new Account(1000);
Thread thread1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
account1.transfer(account2,RandomAmount());
}
},"t1");
Thread thread2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
account2.transfer(account1,RandomAmount());
}
},"t2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("钱相加:"+(account1.getMoney()+account2.getMoney()));
}
}
class Account{
private int money;
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
Account(int money) {
this.money = money;
}
public synchronized void transfer(Account target,int amount){
if(this.money >= amount){
this.setMoney(this.getMoney() - amount);
target.setMoney(this.getMoney() + amount);
}
}
}
这次的结果就比较离谱,参差不齐的。
这题的难点在于要保护的共享变量有两个,一个是this.money,一个是target.money
像代码里只给方法加synchronize是没用的,因为synchronize加上去也只是加在this这个对象上,只保护到this.money,但没法保护到target.money,起不到一个保护的作用。我们得找一个this.money和target.money共享的地方。
用关键字锁住Account类即可。
Monitor概念
Java对象头
由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。
1.对象头形式
通常写的一个Java对象都是由两部分组成,一部分是成员变量,一部分就是对象头。
JVM中对象头的方式有以下两种(以32位JVM为例):
普通类对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
一个对象头在32位下是8个字节,其中KlassWord是一个指针,它指向了对象所从属的class,也就是找到了它的类对象。MarkWord在后面详解。其中数组对象中还有4个字节的数组长度。
MarkWord
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word
的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word
为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold
选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。
Monitor
Monitor被翻译为监视器或者管程。
一、monitor概念
在操作系统中,存在着semaphore和mutex,为了更好的编写并发程序,在mutex和semaphore基础上,提出了更高层次的同步原语,实际上,monitor属于编程语言的范畴,具体的实现模式,不同的编程语言都有可能不一样,C语言不支持monitor,而java支持monitor机制。
一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monitor能够实现互斥的效果。无法进入monitor的临界区的进程/线程,应该被阻塞,并且在适当的时候被唤醒。显然,monitor作为一个同步工具,也应该提供这样管理线程/进程的机制。
monitor这个机制之所以被称为:更高级的原语,它不可避免的需要对外屏蔽这些机制,并且在内部实现这些机制。
二、monitor基本元素
- 临界区
- monitor对象和锁
- 条件变量,以及定义在monitor对象上的wait,notify操作
使用monitor主要是为了互斥进入临界区,为了能够阻塞无法进入临界区的进程,线程,需要一个monitor object来协助,这个object内部会有相应的数据结构,例如列表,用来保存被阻塞的线程;同时由于monitor机制本质是基于mutex原语的,所以object必须维护一个基于mutex的锁。
此外,为了在适当的时候能够阻塞和唤醒 进程/线程,还需要引入一个条件变量,这个条件变量用来决定什么时候是“适当的时候”,这个条件可以来自程序代码的逻辑,也可以是在 monitor object 的内部,总而言之,程序员对条件变量的定义有很大的自主性。不过,由于 monitor object 内部采用了数据结构来保存被阻塞的队列,因此它也必须对外提供两个 API 来让线程进入阻塞状态以及之后被唤醒,分别是 wait 和 notify。
三、monitor在java中的实现
每个Java对象都可以关联一个Moniror对象。如果使用synchronize给对象上锁(重量级)之后,该对象的MarkWord中就被设置指向Monitor对象的指针。(ptr_to_heavyweight_monitor:指向管程Monitor的指针。)
临界区:被synchronized关键字修饰的方法,代码块,就是monitor机制的临界区。
monitor object:在上述synchronized关键字被使用时,往往需要指定一个对象与之关联,例如synchronized(this),总之,synchronized需要管理一个对象,这个对象就是monitor object。
Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据(对象体)和对齐填充,而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制,基本原理如下所示:
在Java中,一个对象对应了一个momitor对象,而synchronized关键字也需要关联一个对象,这个对象需要天生就支持monitor,所以在Java中,可以就是Java 中的 java.lang.Object 类,便是满足这个要求的对象,任何一个 Java 对象都可以作为 monitor 机制的 monitor object。这也就是wait(),notify(),notifyAll(),是在Object类中的原因。
需要注意的是,synchronize的代码块需要关联到同一对象才能产生锁的效果。