多线程访问共享资源,容易导致结果出现错误,如果线程1获取共享资源v= 1,然后对v进行自增操作,变成了2
但是还没有写入共享资源,这时候发生了上下文切换
线程2, 获取了共享资源 v = 1, 然后对v进行自减操作,变成了0,然后写入了共享资源,这时候v = 0
但是线程2执行完之后,时间片就分配回线程1,线程1执行写入操作,最后v = 2
多个线程在临界区内执行,由于代码的执行序列不同 而导致结果无法预测,称之为发生了竞态条件
也就是上面的例子中,发生上下文切换类似的操作,就是发生了竞态条件
synchronized(对象)
{
//临界区
}
synchronized 实际是用 对象锁保证了 临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换打断。
把需要保护的共享变量放入一个类
class Room {
private int value = 0;
public void increment() {
synchronized (this) {
value ++;
}
}
public void decrement() {
synchronized (this) {
value --;
}
}
public int getValue() {
synchronized (this) {
}
}
}
成员方法上的synchronized
class Test {
public synchronized void test() {
}
}
等价于 锁住当前类this对象
class Test{
public void test() {
synchronized(this) {
}
}
}
静态方法上的synchronized
class Test {
public synchronized static void test() {
}
}
等价于 锁住类对象
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
不加synchronized 的方法
不加synchronized 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去) 不能保证原子性
锁住的是同一个对象,所以时间片先分配给谁,先输出谁,所以1 跟 2 都有可能
sleep()不会释放锁,所以情况跟情况1差不多,只是中间多了一个1s的等待
public static void test1() {
int i = 10;
i ++;
}
这个代码不会出现线程安全问题
每一个线程会有自己对应的栈空间,每个线程调用test1()方案时局部变量i,会在每个线程自己的栈空间调用,进行压栈,所以各自调用自己的,互不干扰,不存在共享。
public static void main(String[] args) {
ThreadUnsafe tes = new ThreadUnsafe();
for (int i = 0; i < 2; i ++ ) {
new Thread(() -> {
test.method1(200);
}, "Thread" + (i + 1)).start();
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++ ) {
method2();
method3();
}
}
private void methdo2() {
list.add("1");
}
private void methdo3() {
list.remove(0);
}
}
上面的代码中,list是成员变量,所以是共享资源,也就是临界区,临界区中的代码如果不加以限制,多线程情况下,会造成执行顺序不可预测,发生竞态条件
所以上面的代码会有线程安全的问题,会导致发生数组下表越界异常(同时执行remove操作,这时候就会发生错误)
解决方法
需要确保只能有一个线程能够执行,或者将成员变量 变成 局部变量。
如果创建一个子类 继承 ThreadUnsafe类,然后子类对 method2 或者 method3 进行重写,创建一个新的线程执行
这时候list这个局部变量就暴露了, 也就是在子类中的一个新的线程中被引用到了,这时候list就是一个共享资源,也就是临界区,那么就会发生线程安全问题
public static void main(String[] args) {
ThreadUnsafe tes = new ThreadUnsafe();
for (int i = 0; i < 2; i ++ ) {
new Thread(() -> {
test.method1(200);
}, "Thread" + (i + 1)).start();
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++ ) {
method2();
method3();
}
}
//private
public void methdo2() {
list.add("1");
}
//private
public void methdo3() {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
解决方案:
可以通过将父类中的方法权限修饰符进行修改,变成private或者final等,子类就不能够进行重写,这样就不会导致线程安全问题。
这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的
Hashtable table = new Hashtable();
new Thread(() -> {
table.put("key", "value1");
}).start();
new Thread(() -> {
table.put("key", "value2");
}).start();
可以看源码,是添加了synchronized锁,保证了原子性
但是线程安全是调用单一方法
如果多个方法组合调用 ,那么将就不是线程安全的了
Hashtable table = newHashtable();
if (table.get("key") == null) {
table.put("key", value);
}
线程1跟线程2同时访问上面的代码
单独访问put跟get方法是有原子性的,但是两个组合起来就不是了
String、Integer是不可变类,所以其内部状态是不可修改的,因此他们的方法都是线程安全的
疑问:String 中有 substring等方法不是可以修改他的值吗
substring是创建一个新的值,所以不会对原本字符串进行修改。
继承了HttpServlet,Servlet是Tomcat中的,只能有一个实例,所以omcat中的多线程调用的时候共享使用,就会发生线程安全得问题
例1:
例2:
最好对count有一些保护,防止称为临界区。
例3:
例4:
例5:如果将例4中的Connection 写成成员变量,不是局部变量,那么就会有线程安全问题
因为 servlet 只有一份,导致 userservice只有一份,所以UserDao也只有一份,所以多线程访问的时候,就会导致可能第二个线程close链接,第一个线程就拿不到了。
例6:
所以平时书写的时候,不想往外暴露的就写成final,或者private私有的,可以增强安全性。
String 类是不可变的,但是他也是写成final,防止发生继承之后覆盖行为,修改了。这也是很经典的 闭合原则
例子:
int 占用 4个字节
Integer 占用 8个对象头 + 4 个int值字节 12字节
Monitor 被翻译成 监视器 或 管程(操作系统层面)
注意:
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁 使用者是没有感知的,语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
//同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
//同步块B
}
}
原理:
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变成重量级锁
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
//同步块
}
}
所谓的锁膨胀,也就是在将来的解锁操作 进入一个重量级锁的解锁操作
根据上图进行解释分析:
重量级锁竞争的时候,还可以使用自旋来进行优化,也就是会进行几次循环重试。
如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁),这时候当前线程就可以避免阻塞了。
因为阻塞会导致上下文切换,性能影响比较大。
自旋只有在多核cpu的情况下才有用,如果单核就没有意义。一个cpu执行同步代码块,另一个线程都没有cpu执行循环,所以没有意义
轻量级锁在没有竞争时(就自己当前线程在运行),每次重入仍然需要执行CAS操作,CAS肯定是执行失败,但是知道是自己线程,所以会保留下来,有性能损耗
Java 6 中引入了偏向锁来做进一步优化: 只有第一次使用CAS 将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。
以后只要不发生竞争,这个对象就归该线程所有。
貌似对象的hashcode是懒生成的,当调用hashcode()方法获取 hashcode 值的时候才对对象头里面写入hashcode值,一旦hashcode已经写入,无法使用偏向锁进而使用轻量级锁等
一个对象创建时:
锁使用优先级:
偏向锁 > 轻量级锁 > 重量级锁
线程调用hashcode() 方法之后,根据对象头格式,偏向锁有54位存储线程id,没有多余的地方存储31位的hashcode码,所以会将thread、epoch清空,转成正常Normal对象。
敲黑板:偏向锁和hashcode是互斥存在的;
当一个线程使用了当前对象,如果使用的是偏向锁,那么会在thread中记录当前线程的id,表示这个对象给当前线程用
这时候如果有另一个线程来访问这个对象,这时候发现上面有偏向锁,偏向了某一个线程,这时候会撤销偏向锁,改成轻量级锁,然后记录锁地址。
最后解锁之后,无锁状态。
我们知道加锁,不管了怎么优化,偏向锁,轻量级锁,都会对性能有锁损耗,但是为什么执行代码耗时一样。
这时候就涉及到了JVM了对象逃逸,
我们Java程序是对字节码通过解释 + 编译的方式来执行的,但是对于其中的一些热点代码,会进行一个优化
这时候就涉及 JIT即时编译器 ,会将热点代码进一步翻译成机器码,缓存起来,以后执行就不用 通过编译了
另外他的一个优化手段就是去分析这个局部变量是不是可以优化,发现根本不会逃离方法作用范围,那就不会共享,那么加锁就没有意义,所以Jit 即时编译器会直接将synchronized优化去掉,只是执行了锁中的代码块的代码。
锁消除参数默认开启,如果需要关闭可以使用功能下面 VM 参数
wait()、notify()、 notifyAll() 方法都是属于Object 对象的方法,需要获取此对象的锁 之后才能够使用
wait() 对象调用wait() 方法之后,线程会 Owner中释放锁,然后进入WaitSet中等待唤醒
wait( time ) 对象调用带参数的wait()方法之后,线程会 Owner中释放锁,然后进入WaitSet中等待 指定时间,然后如果期间没有被唤醒,指定之间之后就会自动唤醒,然后进入EntryList再次尝试获取锁,竞争锁
notify () 对象调用notify()方法之后,会随机挑选一个 WaitSet 中的一个线程唤醒,然后进入EntryList 竞争锁
notifyAll() 对象调用notifyAll()方法之后,会 唤醒 WaitSet 中所有线程,然后进入EntryList中竞争锁。
wait(long n) 进入 TIMED_WAITING 状态
wait( ) 进入 WAITING状态
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
//干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
class GuardedObject {
//结果
private Object response;
//获取结果
public Object get() {
synchronized (this) {
//没有结果
while(response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
//产生结果
public void generation(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
System.out.println(guardedObject.get());
}).start();
new Thread(() -> {
int x = 111;
guardedObject.generation(x);
}).start();
}
相对于join的好处
join 需要等待线程执行的结束之后,才能唤醒自己的线程,需要是全局变量 等待另一个线程的结束
保护性暂停等待 设计模式不需要完全等待线程执行结束,可以线程执行到一半的时候就响应线程,从而唤醒自己线程,继续执行,可以是局部变量 等待另一个线程的结果
class GuardedObject {
//结果
private Object response;
//获取结果
public Object get(long timeout) {
synchronized (this) {
//记录一个初始时间
long begin = System.currentTimeMillis();
//经过时间
long access = 0;
//没有结果
while(response == null) {
long waitout = timeout - access;
if (waitout <= 0) {
break;
}
try {
this.wait(waitout);
} catch (InterruptedException e) {
e.printStackTrace();
}
access = System.currentTimeMillis() - begin;
}
return response;
}
}
//产生结果
public void generation(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
join 实现源码跟 超时增强是一模一样的
在java中,Thread类线程执行完run()方法后,一定会自动执行notifyAll()方法。因为线程在die的时候会释放持用的资源和锁,自动调用自身的notifyAll方法。
刚才思路的问题:
一个线程通过一个 GuardedObject 对象来进行通信, 通过参数的形式来传递,多个线程之间传递来传递去,非常不方便
实现解耦:
通过设计一个集合来管理多个 GuardedObject ,每个给予一个id,用于区分,然后 生产供给, 获取所需。
class Mailboxes{
//集合
private static Map<Integer, GuardedObject> map = new Hashtable<>();
//id
private static int id = 1;
private static synchronized int geterateId() {
return id ++;
}
public static GuardedObject createGuardedObject() {
GuardedObject guardedObject = new GuardedObject(geterateId());
map.put(guardedObject.getId(), guardedObject);
return guardedObject;
}
public static GuardedObject getGuardedObject(int id) {
return map.remove(id);
}
//获取所有GuardedObject
public static Set<Integer> getIds() {
// System.out.println(map.keySet());
return map.keySet();
}
}
class GuardedObject {
//id
private int id;
//结果
private Object response;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
//获取结果
public Object get(long timeout) {
synchronized (this) {
//记录一个初始时间
long begin = System.currentTimeMillis();
//经过时间
long access = 0;
//没有结果
while(response == null) {
long waitout = timeout - access;
if (waitout <= 0) {
break;
}
try {
this.wait(waitout);
} catch (InterruptedException e) {
e.printStackTrace();
}
access = System.currentTimeMillis() - begin;
}
return response;
}
}
//产生结果
public void generation(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
class People extends Thread {
@Override
public void run() {
//准备收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
System.out.println("准备收信" + guardedObject.getId());
Object res = guardedObject.get(5000);
//收到信
System.out.println("收到信" + res);
}
}
class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
//开始送信
// System.out.println("送信" + id + "内容" + mail);
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
System.out.println("送信" + id + "内容" + mail);
guardedObject.generation(mail);
}
}
public class Main{
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i ++ ) {
new People().start();
}
Thread.sleep(1);
// System.out.println(Mailboxes.getIds());
for (Integer id : Mailboxes.getIds()) {
// System.out.println(id + "内容");
new Postman(id, id + "内容").start();
}
}
}
特点: 产生结果线程 和 使用结果线程是一一对应的。
为什么这里是异步,保护性暂停模式却是同步?
final class MessageDeque {
private static LinkedList<Message> list = new LinkedList<>();
private static int capacity;
public MessageDeque(int capacity) {
this.capacity = capacity;
}
//存放消息
public void put(Message message) {
synchronized (list) {
//如果没有满
while (list.size() == capacity) {
try {
list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("生产者线程等待结束, 没有满, 放入");
list.addLast(message);
list.notifyAll();
System.out.println("放入结束");
}
}
//取出消息
public Message take() {
synchronized (list) {
//如果没有消息
while(list.isEmpty()) {
try {
list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Message message = list.removeFirst();
System.out.println("消费者等待结束, 拿出消息" + message.getId());
list.notifyAll();
return message;
}
}
}
final class Message {
private int id;
private Object mail;
public Message(int id, Object mail) {
this.id = id;
this.mail = mail;
}
public int getId() {
return id;
}
public Object getMail() {
return mail;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", mail=" + mail +
'}';
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MessageDeque messageDeque = new MessageDeque(2);
for (int i = 0; i < 3; i ++ ) {
int id = i;
new Thread(() -> {
messageDeque.put(new Message(id, "消息" + id));
}).start();
}
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
messageDeque.take();
}
}).start();
}
}
它们是 LockSupport类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复当前线程的运行
LockSupport.unpark(暂停线程对象);
调用了Park方法的线程进入 Waiting的状态
特点:
与Object 的 wait & notify 相比
每个线程都有自己的一个 Parker 对象, 由三部分组成 _counter, _cond 和 _mutex 打个比喻
一把锁本来锁住一个房间,那么一个人要睡觉另一个人要学习就是串行执行
锁的粒度细分
分两把锁,锁住房间,睡觉去睡觉的房间,学习去学习的房间
有这样的情况:一个线程需要同时获得多把锁,这时就容易发生死锁
t1 线程 获得 A对象 锁, 接下来想要获得 B对象 锁
t2 线程获得 B对象 锁,接下来想要获得 A对象 锁
同时阻塞住了,死锁了。
各自等对方筷子,也就对方锁,循环等待,所以死锁了
不断改变对方结束条件,所以适当增加一些随机睡眠时间,使得交错运行,就可以避免活锁。
就是两个线程互相需要对方的锁,那么就死锁了
如果让他们按照一定的顺序加锁,那么就能解决死锁问题了
但是就可能造成饥饿,也就是有些锁获取的次数比较少,概率很低,这就叫做饥饿。
想对于 synchronized 它具备以下特点
与 synchronized 都支持可重入。
synchronized 在关键字级别去保护临界区
ReentrantLock 在对象级别去保护临界区, 需要创建一个 ReentrantLock 对象
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
一个线程自己已经获取到锁了,第二次想要获取这把锁
线程 等待锁的过程中,可以使用 interrupt 方法终止线程的等待。
synchronized 和 ReentrantLock.lock 都是不可打断的
需要使用 ReentrantLock.lockInterruptibly() 才是可以调用线程的 interrupt 方法打断的。
可打断看,可以避免死等,减少死锁发生 。
ReentrantLock.tryLock() 获取不到锁立刻返回false, 否则 返回 true
ReentrantLock.tryLock(long n, TimeUnit) 获得不到锁就等待时间 【n TimeUnit】 内一直重试,时间结束之后返回false, 获取到了就返回true
可以调用 interrupt 方法打断 tryLock 方法,支持可打断。
带超时的方法都是避免 无限制 的等待
可以使用tryLock 解决 哲学家就餐问题。
获取不到锁(筷子)就放下,互相谦让,最后和气吃饭。
ReentrantLock 默认 不公平,通过构造方法传入 参数 true 表示公平
可以让 进入 EntryList中的线程按照先进先出的原则,避免饥饿现象发生
公平锁一般没有必要,会降低并发度,后面分析原理会讲解。
直接说 条件变量 不好理解,举例讲解
synchronized 中也有条件变量, 当不满足条件时,进入WaitSet中等待。
ReentrantLock 相对于 synchronized 优势在于,支持多个条件变量,
创建条件变量
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
COndition condition2 = lock.newCondition();
lock.lock();
try {
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
new Thread(() -> {
lock.lock();
try {
condition1.signal();
} finally {
lock.unlock();
}
});
static Object lock = new Object();
static boolean t2runned = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized(lock) {
while (!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(1);
}
}, "t1").start();
new Thread(() -> {
synchronized(lock) {
System.out.println(2);
t2runned = true;
lock.notify();
}
}, "t2").start();
}
public class Main {
static ReentrantLock lock = new ReentrantLock();
static boolean flag = false;
static Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
lock.lock();
try {
while (!flag) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(1);
} finally {
lock.unlock();
}
});
t1.start();
new Thread(() -> {
lock.lock();
try {
System.out.println(2);
flag = true;
condition.signal();
} finally {
lock.unlock();
}
}).start();
}
}
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
System.out.println(1);
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
System.out.println(2);
LockSupport.unpark(t1);
}, "t2").start();
}
线程1输出a 5次,线程2 输出 b 5次, 线程3输出c 5次, 现在要求输出abc abc abc abc abc 怎么实现