字符串池
举个例子:
String a = "a" + "b";
System.out.println(a);
System.out.println(a.intern());
System.out.println(a == a.intern());
String b = new String("ja" + "va");
System.out.println(b);
System.out.println(b.intern());
System.out.println(b == b.intern());
上面的例子,一个返回true,一个返回false
原因是:
System类初始化的时候会声明一个java字符串常量
private static final String launcher_name = “java”;
采用new创建的字符串对象不进入字符串池
intern方法:返回一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。
尽管在输出中调用intern方法并没有什么效果,但是实际上后台这个方法会做一系列的动作和操作。
在调用”ab”.intern()方法的时候会返回”ab”,但是这个方法会首先检查字符串池中是否有”ab”这个字符串,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。
推荐阅读:深入理解Java虚拟机——JVM高级特性与最佳实践
可重入锁
可重入锁:又名递归锁,是指同一个线程在外层方法获得锁的时候,再进入该线程的内层方法会自动获取锁(前提:锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞,也就是同一个线程可以多次获取同一把锁。
synchronized和ReentrantLock就是可重入锁,可重入锁的一个优点就是可一定程度上的避免死锁
/*
synchronized可重入锁的例子
*/
public static final Object OBJECT = new Object();
/*
ReentrantLock的可重入锁的例子
*/
static Lock lock = new ReentrantLock();
public static class M1 implements Runnable {
@Override
public void run() {
synchronized (OBJECT) {
System.out.println("外层调用");
synchronized (OBJECT) {
System.out.println("中层调用");
synchronized (OBJECT) {
System.out.println("内层调用");
}
}
}
}
}
public static void main(String[] args) {
M1 m1 = new M1();
Thread thread = new Thread(m1);
thread.start();
new Thread(() -> {
lock.lock();
try {
System.out.println("外层锁");
lock.lock();
try {
System.out.println("内层锁");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}).start();
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加一
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么java虚拟机可以将其计数器加一,否则需要等待,直至持有线程释放该锁
当执行monitorexit时,java虚拟机则需将锁对象的计数器减一,计数器为零则代表该锁已经被释放
LockSupport
LockSupport类:线程等待唤醒机制,park和unpark,阻塞线程和解除线程阻塞
问题:我们已经有了wait/notify和await和single,为什么还需要park/unpark方法?
答案:因为wait/notify和await和single都有限制:
1、线程要先获得并持有锁,且必须在同步代码块中才可以。
2、必须要先等待后唤醒,线程才能被唤醒
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞以后也有对应的唤醒方法,归根结底,LockSupport调用的Unsafe中的native代码
LockSupport和每个使用它的线程都有一个许可(permit)关联,permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park就会消费permit,也就是将1变成0,同时park立即返回
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时会调用unpark会把permit置位1。每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证
所以我们就可以回答下面的问题了:
问:为什么可以先唤醒线程后阻塞线程?
答:因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,而不会阻塞了
问:为什么唤醒两次后阻塞两次,但是最终结果还是会阻塞线程?
答:因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证,而调用两次park却需要消费两个凭证,证不够,不能放行,所以被阻塞
public static void main(String[] args) {
Thread a = new Thread(() -> {
try {
System.out.println("开始阻塞3秒钟");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\tcome in");
LockSupport.park();
System.out.println("被唤醒");
}, "a");
a.start();
Thread b = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\tcome in");
LockSupport.unpark(a);
System.out.println("通知a唤醒");
}, "b");
b.start();
}
AQS
AQS:AbstractQueuedSynchronizer(抽象的队列同步器)
线程阻塞机制:排队等候机制
int变量:state状态,0代表无线程占用,1代表有线程占用
CLH队列:双向队列,里面有Node节点存储Thread
Node类:头指针,尾指针,前指针,后指针,waitStatus(每个线程排队的状态)
ReentrantLock的公平锁和非公平锁的区别就在于hasQueuedPredecessors方法,这个方法判断了是否需要排队
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
具体的参考实现可以参考这篇博客:https://www.cnblogs.com/waterystone/p/4920797.html