13.烧水泡茶
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
public void run() {
System.out.println("洗水壶");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("烧开水");
try {
sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread() {
public void run() {
System.out.println("洗茶壶");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("洗茶杯");
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("拿茶叶");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t1.join();//等待t1执行完,等水烧好.
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("pao cha");
}
};
t1.start();
t2.start();
}
这种接法的缺陷:
上面的解法是等t1水烧开后,t2泡茶,如果反过来要实现t1等t2拿过来的茶叶,由t1来进行泡茶呢? 没有满足第二种情况.
上面的线程各自执行,如果要模拟t1把水壶给t2泡茶,或者模拟t2把茶叶交给t1泡茶.
后面要学习的内容有:
- 共享问题.
- synchronized
- 线程安全分析
- Monitor
- wait/notify
- 线程状态转换
- 活跃性
- Lock
14.临界区
- 一个程序运行多个线程本身是没有问题的.
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题.
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题.
- 一段代码块内如果存在对共享资源的多线程读写操作,这称这段代码块为临界区.
14.1竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件.
例如:t1,t2线程交替执行,有可能会导致结果不正确.
static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread() {
public void run() {
for(int i=0;i<5000;++i) {
count++;
}
}
};
Thread t2=new Thread() {
public void run() {
for(int i=0;i<5000;++i) {
count--;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
14.2synchronized解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的.
- 阻塞式的解决方案,synchronized,Lock.
- 非阻塞式的解决方案:原子变量.
本次课使用阻塞式的解决方案,:synchronized,来解决以上问题,即俗称的[对象锁],它采用了互斥的方式让同一时刻至多只有一个线程能持有[对象锁].其他线程再想获取这个[对象锁]时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换.
synchronized语法:
synchronized(对象){ 临界区 }
14.3关于synchronized的思考
synchronized实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的.不会被线程的切换所打断,
为了加深理解.请细品一下3个问题.
- 如果把synchronized(obj)放在for循环之外呢,如何理解?
Thread t1=new Thread() {
public void run() {
synchronized (Lock) {
for(int i=0;i<5000;++i)
count++;
}
}
};
synchronized的作用就是使得保护的临界区原子化,如果synchronized放在for的外面,那么就意味着for循环内的代码具有了原子性,在执行不完for中的代码前,是不会解锁的. ----强调原子性
- 如果t1 synchronized(obj1)而t2 synchronized(obj2) 会怎样运作?
对象锁不同,要保护共享资源需要多个线程锁住的是同一个对象,--强调锁对象
- 如果t1 synchronized(obj) 而t2并没有加锁,会怎么样?
t2如果不加锁,等于没有限制,,就不会去获取锁对象,也就不会被阻塞(Blocked),就可以所以的操作共享资源.
14.4锁对象---面向对象改进
public class Test15 {
static int count=0;
//static Object Lock = new Object();
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1=new Thread() {
public void run() {
for(int i=0;i<5000;++i) {
room.increament();
}
}
};
Thread t2=new Thread() {
public void run() {
for(int i=0;i<5000;++i) {
room.decreament();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(room.getCount());
}
}
class Room{
private int count=0;
public void increament() {
synchronized (this) {
count++;
}
}
public void decreament() {
synchronized (this) {
count--;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
14.5方法上的synchronized
加在非静态方法上的synchronized
public synchronized void test() {
}
等价于
public void test1() {
synchronized (this) {
}
}
加在静态方法上的synchronized
public synchronized static void test2() {
}
等价于
public static void test2() {
synchronized (Test16.class) {
}
}
注意:synchronized加在了非静态方法上了,并不是意味着将方法锁住了,而是方法里面的对象(this)
加在了静态方法上等价于是锁住了类对象.
不加synchronized的方法就好比不遵守规则的人,不老实排队(去翻窗子进去)
15.线程八锁.
其实主要考察的就是synchronized锁住的是哪个对象?
如果是相同的对象则会存在互斥,.如果是不同的对象则不存在互斥,直接按照线程并发执行处理..
15.1第一锁.
public class Lock1 {
public static void main(String[] args) {
Number n1 = new Number();
//启动线程1
new Thread() {
public void run() {
n1.a();
}
}.start();
//启动线程2
new Thread() {
public void run() {
n1.b();
}
}.start();;
}
}
class Number{
public synchronized void a() {
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
- 分析:首先synchronized锁的是Number.class的成员方法:a(),b();查看锁对象是不是this.
- 查看:两个线程都是通过n1来进行调用的,-->表明this相同(即当前的锁对象相同)---->>是存在互斥关系的.
- 因此:CPU调度先调度哪个线程,哪个线程就先执行.
- 执行结果:有两种情况
- 12(因为第一个线程先启动,所以出现,12的概率大.)或者21
15.2第二锁
public class Lock2 {
public static void main(String[] args) {
Number2 n1 = new Number2();
//启动线程1
new Thread() {
public void run() {
System.out.println("线程1启动");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
n1.a();
}
}.start();
//启动线程2
new Thread() {
public void run() {
System.out.println("线程2启动");
n1.b();
}
}.start();;
}
}
class Number2{
public synchronized void a() {
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
- 分析:首先synchronized锁的是Number2.class的成员方法:a(),b();查看锁对象是不是this.
- 查看:两个线程都是通过n1来进行调用的,-->表明this相同(即当前的锁对象相同)---->>是存在互斥关系的.
- 因此:CPU调度先调度哪个线程,哪个线程就先执行.
- 执行结果:有两种情况
- 12(因为第一个线程先启动,所以出现,12的概率大.)或者21
- sleep()方法并不能释放锁.
15.3第三锁
public class Lock3 {
public static void main(String[] args) {
Number3 n1 = new Number3();
//启动线程1
new Thread() {
public void run() {
System.out.println("线程1启动");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
n1.a();
}
}.start();
//启动线程2
new Thread() {
public void run() {
System.out.println("线程2启动");
n1.b();
}
}.start();
//启动线程3
new Thread() {
public void run() {
System.out.println("线程3启动");
n1.c();
}
}.start();
}
}
class Number3{
public synchronized void a() {
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
public void c() {
System.out.println("3");
}
}
- 分析:首先synchronized锁的是Number3.class的成员方法:a(),b();查看锁对象是不是this c()方法无锁.
- 查看:两个线程都是通过n1来进行调用的,-->表明this相同(即当前的锁对象相同)---->>是存在互斥关系的.但是线程3没有锁.
- condition1:如果线程1首先拿到了时间片,t2阻塞,t2在等待t1释放锁.线程1进入一秒的睡眠后,因为线程3并没有加锁,线程3可以就可以争取到了时间片,进行输出.
- 结果:3 12
- condition2:如果是线程2首先拿到了时间片,线程1会阻塞,但是线程3无锁,那么意味着,线程3可以和线程2竞争使用权,
- 线程2拿到使用权并输出,然后是线程3,线程2执行完毕,无论是线程3还是线程1抢到时间片,但是因为线程1会休眠,所以线程3总是在线程1之前输出的.即:321,231.
15.4第四锁
public class Lock4 {
public static void main(String[] args) {
Number4 n1 = new Number4();
Number4 n2 = new Number4();
//启动线程1
new Thread() {
public void run() {
System.out.println("线程1启动");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
n1.a();
}
}.start();
//启动线程2
new Thread() {
public void run() {
System.out.println("线程2启动");
n2.b();
}
}.start();
}
}
class Number4{
public synchronized void a() {
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
- 分析:首先两个线程获取到的对象锁都不是同一个,即不存在冲突问题.按照普通并发处理.
- 因为线程1在输出前总要睡眠一秒.所以输出结果:总是.21.
15.5第五锁
public class Lock5 {
public static void main(String[] args) {
Number5 n1 = new Number5();
//线程1
new Thread() { public void run() {try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}}}.start();
//线程2
new Thread() { public void run() {n1.b();}}.start();;
}
}
class Number5{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
- 分析:因为两个线程一直执行的是静态方法,一个是成员方法.锁对象不同,因此无互斥.
- 输出:2 1
15.6第六锁
public class Lock6 {
public static void main(String[] args) {
Number6 n1 = new Number6();
//线程1
new Thread() { public void run() {try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}}}.start();
//线程2
new Thread() { public void run() {n1.b();}}.start();;
}
}
class Number6{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
System.out.println("1");
}
public static synchronized void b() {
System.out.println("2");
}
}
- 分析:首先synchronized锁住的都是两个静态方法.并且锁住的类对象都是一样的.都是Number6.class,因此存在互斥.
- 结果:1 2或者2 1
15.7第七锁
public class Lock7 {
public static void main(String[] args) {
Number7 n1 = new Number7();
Number7 n2 = new Number7();
//线程1
new Thread() { public void run() {try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}}}.start();
//线程2
new Thread() { public void run() {n2.b();}}.start();;
}
}
class Number7{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
- 分析:首先synchronized分别锁住的是静态方法和成员方法.锁对象不同,一个是 类对象,一个是this(n2对象) 因此不存在互斥.
- 结果:2 1
15.8第八锁
public class Lock8 {
public static void main(String[] args) {
Number8 n1 = new Number8();
Number8 n2 = new Number8();
//线程1
new Thread() { public void run() {try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}}}.start();
//线程2
new Thread() { public void run() {n2.b();}}.start();;
}
}
class Number8{
public static synchronized void a() throws InterruptedException {
Thread.sleep(1000);
System.out.println("1");
}
public static synchronized void b() {
System.out.println("2");
}
}
- 分析:synchronized锁住的是两个静态方法,这两个静态方法都输出Number8.class的对象.因此是相同的类变量.因此存在互斥
- 结果:1 2或者2 1
16.变量的线程安全分析
16.1成员变量和静态变量是否线程安全?
- 如果它们没有被共享,则是线程安全的.
- 如果他们被共享了.根据它们的状态能否被改变又分为两种情况.
- 如果只有读操作,则线程安全.(因为读操作不影响,内存中的值.)....但是会出现脏读
- 如果有读写操作.,则这段代码的临界区,需要考虑线程安全了.
16.2局部变量是否线程安全?
- 局部变量是线程安全的.
- 但局部变量引用的对象则未必,
- 如果该对象没有逃离方法的作用访问,它是线程安全的.
- 如果该对象逃离方法的作用范围,需要考虑线程安全.
16.2.1局部变量线程安全分析.
public static void test1() {
int i = 10;
i++;
}
- 每个线程在调用test1()时,局部变量i,会在每个线程的桢栈内存中会被创建多份,因此不存在共享
- 局部变量实现线程私有的.(不共享就安全)
16.2.1局部变量引用的安全分析
public class TestThreadSafe {
static final int THREAD_NUMBER=2;
static final int LOOP_NUMBER=200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for(int i=0;i<THREAD_NUMBER;i++) {
new Thread() {
public void run() {
test.method1(LOOP_NUMBER);
}
}.start();
}
}
}
class ThreadUnsafe{
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for(int i = 0 ; i <loopNumber ;i++) {
method2();
method3();
}
}
private void method2() {list.add("1");}
private void method3() {list.remove(0);}
}
- 有时候会出现:Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: -1
- at java.util.ArrayList.add(ArrayList.java:459)
- 分析:无论在哪一个线程中,method2引用的都是同一个对象中的list成员变量.(共享了)
将list修改为局部变量.代码如下
class ThreadSafe{
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for(int i =0;i<loopNumber;i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {list.add("1");}
public void method3(ArrayList<String> list) {list.remove(0);}
}
- 分析:list是局部变量,每个线程调用时会创建其不同实例,没有共享.
- 而method2的参数是从method1中传递过来的,与method1中引用同一个对象,
- method3的参数分析与method2相同
方法访问修饰符带来的思考,如果把method2和method3的方法修改为public会不会出现线程安全问题?
- 情况1:有其它线程调用method1和method2
- method1,2有可能被其他线程调用,,比如线程1调用method1,线程2调用method2,因为线程1中的list是局部变量,私有的,线程2调用method2需要传入一个list,那么就是另外一个不同于method1中的list了.因此不存在线程安全问题.(各玩各的没事)
- 情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖 method2或method3方法
- 子类复写了method3并在其中又开启了一个线程,这就有问题了,这等于是两个线程玩同一个list.有可能会出事的.
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list){
new Thread() {
public void run() {
list.remove(0);
}
}.start();
}
}
- 因此我们得出:方法的访问修饰符是有意义的,private,final使得子类不能影响父类的行为.在一定程度上加强了线程安全性.
- 从这个例子中可以看出private 和final 提供的[安全]的意义所在,细品开闭原则中的[闭];
17.常见的线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.until.concurrent包下的类
17.1常见类的组合调用
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法,是线程安全的.也可以理解为:
- 它们的每个方法是原子的.
- 但注意它们多个方法的组合不是原子的.见后分析.
-
public static void main(String[] args) throws InterruptedException { Hashtable<String,String> table = new Hashtable<String, String>(); //线程1 Thread t1 = new Thread() { public void run() { if(table.get("key") == null) { table.put("key", "t1"); } } }; //线程2 Thread t2 = new Thread() { public void run() { if(table.get("key") == null) { table.put("key", "t2"); } } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(table.get("key")); }
分析:
17.2常见类-不可变.
String,Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的.
内部状态不可变?那String.class里面有substring方法,replace方法.
分析:查看了String中substring方法,发现它并没有改变当前字符串,而是新建了一个新的String对象,在原有字符串的基础之上进行复制,截取的.
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//在原有字符串上进行复制.截取的
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
replace也是一样的机制.
实例分析:
//是否线程安全?
Map<String,Object> map = new HashMap<>();//不安全
String s1 = "...";
final String s2 = "...";
Date d1 = new Date();
final Date d2 = new Date();
public void doGet(HttpServletRequest req,HttpServletResponse res) {
//使用上述变量
}
- Map不是线程安全的,Hashtable是线程安全的.
- String是线程安全的.Date不是线程安全的.
- final Date d2只能确保.d2的引用不会发生改变,但是日期内部的属性是可以发生修改的.(存在读写操作);
例2:
public abstract class Test19 {
public void bar() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract void foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test19().bar();
}
}
- 分析:因为foo的行为不确定,可能导致不安全发生,这种情况被称为外星方法.
- 在复写父类foo中开启另一个线程,操作
- String为什么是final的.
- 这是为了避免子类继承String,因为有些子类覆盖父类的方法后,可能会出现线程不安全的情况,
17.3购票的例子。
public class Test1 {
public static void main(String[] args) throws InterruptedException {
TicketWindow window = new TicketWindow(10000);
//卖出的票数统计
List<Integer> amountList = new Vector<>();
//所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 开始卖票
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread() {
public void run() {
int sellNumber=window.sell(getRandom());
amountList.add(sellNumber);
}// run-end
};
threadList.add(thread);
thread.start();
} // for-end
//等待所有线程执行完毕。
for(Thread thread:threadList)
thread.join();
//卖出去的票数
System.out.println("余票:"+window.getCount());
System.out.println("卖出去的票数:"+amountList.stream().mapToInt(i->i).sum());
}// main-end
// Random为线程安全的
static Random random = new Random();
public static int getRandom() {
return random.nextInt(5) + 1;
}
}
class TicketWindow {
private int count;
//构造器
public TicketWindow(int count) {
this.count=count;
}
//获得当前票数
public int getCount() {
return this.count;
}
//售票
public int sell(int count) {
if(this.count>=count) {
this.count-=count;
return count;
}
//不够买或者卖完了.
else
return 0;
}
}
threadList是属于mian线程的,不会被多个线程调用到,因此是线程安全的.
两个不一样的对象调用的组合,如果单个都是线程安全的,组合到一起也是线程安全的.
对照hashtable的put,get.因为hashtable的调用对象都是同一个对象,尽管,它的put,get都是线程安全的,但是组合起来使用会出现不安全的情况.
17.4转账问题
public class ExerciseaTransfer {
static Random random = new Random();
public static int randomAmount() {return random.nextInt(100)+1;}
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b=new Account(1000);
Thread t1 = new Thread() {
public void run() {
for(int i=0;i<1000;i++) {
a.transfer(b,randomAmount());
}
}
};
Thread t2 =new Thread() {
public void run() {
for(int i=0;i<1000;i++) {
b.transfer(a, randomAmount());
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("A账户的钱:"+a.getMoney());
System.out.println("B账户的钱:"+b.getMoney());
}
}
//用户类
public class Account {
private int money;
public Account(int money) { this.money=money;}
public int getMoney() {return this.money;}
public void setMoney(int money) {this.money=money;}
//转账
public void transfer(Account targer,int amount) {
synchronized(Account.class) {
if(this.money>=amount) {
this.setMoney(this.money-amount);
targer.setMoney(targer.getMoney()+amount);
}
}
}
}
- 分析:线程不安全一定会出现在transfer方法内.(因为里面做了读写操作),
- 解决办法:1.将两个类都添加上锁.(不可行:因为这样有可能会导致死锁)
- 2.提取通性,对Account.class进行加锁.(但是效率不高.只能对两个账户进行操作,其他的用户转账只能等待)
18.Monitor概念
18.1Java对象头- 也可以理解为计网中的数据报的首部信息
以32位虚拟机为例
- 普通对象: Object Header(64bit)=Mark Work(32bit)+Klass Word(32bit)
- 数组对象: Object Header(96bit) = Mark Work(32bit)+Klass Word(32bit) + Array Length(32bit)
- Mark Word 主要是用来
- Klass Word只要是用来找到该对象的类对象的.
18.2Monitor(锁或者叫做管程)
每个Java对象都可以关联一个Monitor对象.如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置为指向Monitor对象的指针.
Monitor结构如下:
- 刚开始Monitor中的Owner为null;
- 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner设置为Thread-2,Monitor中有且只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList中 阻塞.
- Thread-2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的.(即先来不一定先得)
- 图中WaitSet中的Thread-0,Thread-1是之前获得过锁的,但条件不满足进入WAITING状态的线程.
- 注意
- synchronized必须是进入同一个对象的monitor中才会上述保护效果.
- 不加synchronized的对象不会关联监视器,自然不会遵从上述规则.
19.Synchronized原理の进阶
19.1轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的.(即没有出现竞争),那么可以使用轻量级锁来进行优化.
轻量级锁对使用者是透明的,即语法仍为:synchronized(...){...}
假设有两个方法同步块,利用同一个对象加锁,
static final Object obj= new Object();
public static void method1(){synchronized(obj){
...
method2();
}}
public static void method2(){synchronized(obj){
....
}}
- 创建锁记录(Lock Record)对象,每个线程的桢栈中都会包含一个锁记录的结构体,内部可以存储锁定对象的Mark Word.
- 让锁记录中Object refrenence指向锁对象,并尝试用cas替换Object 的Mark Word ,将Mark Word的值存入锁记录中.
- 如果cas替换成功,对象头中存贮了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:
- 如果cas失败有两种情况:
- 如果是其他线程已经持有了该对象的轻量级锁,这时表明有竞争.进入锁膨胀过程
- 如果是自己执行了synchronized锁重入,那么再添一条Lock Record作为重入的计数.只不过重入的所记录的MarkWord为null.
- 当退出synchronized代码块(解锁时)如果有取值为null的记录,表示有重入,这时重置锁记录,表示重入计数减一;
- 当退出synchronized代码块时的锁记录不为空,这时使用cas将Mark Word的值回复给对象头
- 成功,解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,那就进入了重量级锁的解锁流程.
- 轻量级锁没有阻塞功能,因此转入重量级锁中就包含了阻塞功能(EntryList)这也可能是出于这个原因
- 这时Thread-1线程加轻量级锁失败了,接着进入了锁膨胀状态.
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址.
- 然后自己进入Monitor的EntryList中阻塞,进行等待.
- 当Thread-0退出同步块解锁时,使用cas将MarkWord的值回复给对象头,结果失败了.因为此时的锁已经升级为重量级锁,会进入重量级锁的解锁流程.即按照Monitor地址找到Monitor对象,设置Ower为空,并且唤醒EntryList中的阻塞的线程.
19.2自旋优化
重量级锁在竞争时,还可以使用自旋来进行优化,如果当前线程自旋成功,(即这时持锁线程已经退出了同步块释放了锁.),这时可以避免阻塞.但是如果自旋重试仍然没有获得资源也会进行阻塞.
通俗的讲:别人再用,你也想用,但是一个时刻只能一个人用,所以你会在等的过程中过去看看用完没.如果刚好用完了,你无缝衔接.节约了整体的时间.如果过去看的次数到达一定的闸值,等的人就不过去看了.乖乖的在原地等(阻塞).
在Java6之后自旋锁变成了自适应的,比如对象最近一次自旋成功,那么它就会认为目前的条件适合自旋,它就会多自旋几次.反之,就会少自旋甚至不自旋.
此外,自旋的话或占用Cpu,单核条件下自旋等于浪费时间.多核条件下才能发挥其优势.
java7之后,不能控制自旋的开关了.
19.3偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行cas操作.
Java6中引入了偏向锁来进一步优化:只有第一次使用cas将线程ID设置到MarkdWord中,之后发现这个线程ID是自己就表示没有竞争.不用重新Cas操作.以后只要不发生竞争.这个对象就归该线程所有.
轻量级锁:
偏向锁:
对比:偏向锁省去了Cas操作,只需要一个if判断即可.
当一个对象被创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,MardWord值的最后三位:101,其中第一个1代表启用偏向锁,01表示无锁状态.并且此时的,thread,epoch,age都为0
- 偏向锁默认是延迟的,不会在程序启动时立刻生效,如果想立刻生效,可以添加JVM参数, -XX:BiasedLockingStartupDelay=0来去掉时延.
- 如果没有开启偏向锁,那么对象创建后,markword最后3位为:001,hashcode,age都为0,
- 处于偏向锁的对象解锁之后,线程id仍存储于对象头中.
- 禁用偏向锁的JVM参数: -XX :-UseBiasedLocking禁用偏向锁
- 偏向锁是优化后的轻量级锁,可以减少一次cas操作,提高性能.
- 调用hashcode会使得偏向锁禁用,变成不可偏向的.
- 优先级:有偏向锁会用偏向锁,如果有其他线程使用了锁对象会变为轻量级锁,如果发生竞争会升级为重量级锁.
19.3.1撤销偏向锁--调用对象的HashCode
调用了对象的HashCode,但偏向锁的对象MarkWord中存储的是线程ID,如果调用对象的Hashcode会导致偏向锁的线程ID被覆盖,即偏向锁被撤销了.
- 轻量级锁会在锁记录(Lock Record)中记录hashCode
- 重量级锁会在Monitor中记录hashCode
19.3.2撤销偏向锁---其他线程使用对象
当有其他线程要使用偏向锁对象时,会将偏向锁升级为轻量级锁,
偏性锁:本来我这个是个t1线程专用的,这又来了一个,使得我来回偏向比较累(耗费Cpu)..那就换锁(轻量级锁)呗.
19.3.3撤销偏向锁---调用wait/notify方法.
因为wait和notify方法是唤醒阻塞线程用的,那就需要EntryList.它们两个是重量级锁才拥有的特性.
19.4批量重偏向
如果对象被多个线程访问,但是没有发生竞争,这时偏向了线程T1的对象仍然有机会重新偏向T2,重偏向后会重置对象的Thread-ID
当撤销偏向锁的次数超过20次后,jvm会觉得我是不是偏向错了,于是会再给这些对象加锁时重新偏向至加锁线程.
19.5批量撤销
当撤销偏向锁闸值超过40次后,jvm会觉得自己确实偏向错了,于是将整个类所有的对象设为不可偏向.新建的对象也不可偏向.
20.锁消除
static int x=0;
public void a(){x++;}
public void b(){
Object o = new Object();
synchronized(o){
x++;
}
}
逃逸分析,b方法中的synchronized根本不会逃离方法体中,因此不会被共享.所以这个同步锁也就没有必要了,Jvm对其进行了优化在实际执行过程中去除了锁..因此a,b方法的耗费时间几乎一致.
21.wait/notify
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态.
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片.
- BLOCKED线程会在Owner线程释放锁时唤醒.
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争锁.
21.1 API的介绍
- obj.wait()让进入object监视器的线程到WaitSet中等待(会释放锁,离开Owner)
- obj.notify(),在object上正在WaitSet等待的线程中挑一个唤醒(随机).
- obj.notifyAll(),唤醒所有在object上正在WaitSet中等待的线程.
- 它们都是线程之间进行协作的手段,都属于Object对象的方法.必须获得此对象的锁,才能调用这几个方法.
问题:wait和sleep的区别?
wait会释放掉Owner(锁),而sleep不会释放Owner(锁).
- 带参数的wait(int n)方法. n的作用就是等n毫秒后自动唤醒.也可以被notify/notifyAll提前唤醒.
- wait();实际上就是在内部又调用了一次wait(0); //0代表一直等,直到被notify/notifyAll唤醒.
22.wait/notify的正确姿势
sleep(long n)和wait(long n)的区别
- sleep是Thread的静态方法,而wait是Object的方法.(从API来源の异)
- sleep不需要强制和synchronized配合使用,但wait必需要和synchronized一起用.因为wait只有Owner才能调用.必需先要持有锁.
- sleep在睡眠同时,不会释放对象锁.但wait在等待时会释放对象锁.(失去Owner进入WaitSet中等待唤醒.)
- 它们的状态都是TIMED_WATING (相同之处)
synchronized(lock){
while(条件不成立){
lock.wait;
}
//干活
}
//另一个线程
synchronized(lock){
lock.notifyAll();//全唤醒
}