同步
。没错,只要使用到多线程,咱们就要考虑同步,不然就乱套了!在同步问题中,java有一个亲儿子——synchronized
关键字。在jdk 1.5后,它就有了一些孪生兄弟 ——JUC
包下的各种锁实现。它们之间的特点将在后续的文章中做出总结。/**
* 模拟4个窗口卖20张票
*/
public class TestMulThread {
private static int ticketNum = 10;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <4; i++) {
new Thread(() -> {
while (!Thread.currentThread().isInterrupted() && ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + ticketNum-- + "张票");
}
}, "窗口" + (i + 1)).start();
}
}
}
运行上段代码,你会发现输出的结果毫无规律,可能出现票号为负数的情况,也有可能出现卖出重复票的情况(本人电脑cpu为12核的,处理速度比较快,不会出现上述情况
)。这明显是有问题的。要解决这个问题我们可以使用同步策略,所谓同步策略即是使用synchronized关键字(这里只考虑synchronized关键字,不考虑其他的情况)。于是,我们进行代码修改,如下所示:
/**
* 模拟4个窗口卖20张票
*/
public class TestMulThread {
private static int ticketNum = 10;
static Lock lock = new Lock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <4; i++) {
new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
synchronized (lock) {
if (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + ticketNum-- + "张票");
} else {
Thread.currentThread().interrupt();
}
}
}
}, "窗口" + (i + 1)).start();
}
}
}
class Lock {
}
修改后的代码为,新建了一个Lock类作为锁对象。这样就完成了同步的操作。下面总结下synchronized关键字的常见使用方式、经典案例及其特点。synchronized锁住的是对象,是通过一个标识来表示是具体的哪一种锁
// 情况一:锁object对象
public class Demo {
private Object object = new Object();
public void test(){
synchronized (object) {
System.out.println(Thread.currentThread().getName());
}
}
}
// 情况二: 锁当前对象 this,锁定某个代码块
// 使用此种方式要注意调用进来的this是否为同一对象
// 若Demo的实例不是单例的,那么这把锁基本上起不到同步的作用
public class Demo {
public void test() {
//synchronized(this)锁定的是当前类的实例,这里锁定的是Demo类的实例
synchronized (this) {
System.out.println(Thread.currentThread().getName());
}
}
}
// 情况三: 锁当前对象 this,锁定整个方法
// 与情况二类似,但是它是锁住了整个方法,粒度比情况二大
public class Demo {
public synchronized void test() {
System.out.println(Thread.currentThread().getName());
}
}
//
// ===> 当调用当前类的所有同步静态方法将会等待获取锁
// 注意: 但是此时还是能调用类实例的同步方法。为什么呢?
// 因为静态同步方法和类实例同步方法拥有的锁不一样
// 一个是类对象一个是类实例对象。
// 同时,此时还能调用类对象的静态非同步方法以及类实例的
// 非同步方法。为什么呢?因为这些方法没有加锁啊,可以直接调用。
public class Demo {
public static synchronized void test() {
System.out.println(Thread.currentThread().getName());
}
}
/**
上面说了,synchronized关键字锁的是对象,
而对于s1和s2这两个对象,他们的值都是lock,
也就是放在常量池中的(堆内的方法区),
所以s1和s2指向的是同一个对象。所以
下面的test1和test2方法使用的都是同一把锁,
最终的运行结果就是线程2会等待线程1把锁释放完毕后
才能获取锁并执行如下代码。
*/
public class Demo {
String s1 = "lock";
String s2 = "lock";
public void test1() {
synchronized (s1) {
System.out.println("t1 start");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 end");
}
}
public void test2() {
synchronized (s2) {
System.out.println("t2 start");
}
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(demo :: test1, "test1").start();
new Thread(demo :: test2, "test2").start();
}
}
public static class BadLockOnInteger implements Runnable{
public static Integer i = 0;
static BadLockOnInteger instance = new BadLockOnInteger();
@Override
public void run() {
for (int j = 0; j <10000000; j++) {
synchronized(i) {
// 在jvm执行时, 这是这样的一段代码: i = Integer.valueOf(i.intValue() + 1),
// 跟踪Integer.valueOf()源码可知, 每次都是返回一个新的Integer对象, 导致加锁的都是新对象,当然会导致多线程同步失效
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
/**
一个同步方法调用另外一个同步方法,支持可重入
*/
public class Demo {
public synchronized void test1() {
System.out.println("test1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test2();
}
public synchronized void test2() {
System.out.println("test2 start");
}
public static void main(String[] args) {
Demo demo = new Demo();
demo.test1();
}
}
/**
继承也支持可重入特性
*/
public class Demo {
synchronized void test() {
System.out.println("demo test start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("demo test end");
}
public static void main(String[] args) {
new Demo2().test();
}
}
class Demo2 extends Demo {
@Override
synchronized void test() {
System.out.println("demo2 test start");
// 此处调用了父类的方法
super.test();
System.out.println("demo2 test end");
}
}
加锁代码块执行结束或者抛出的异常
偏向锁、轻量锁、重量锁
。而这些所谓锁对应的仅仅是对象头的一些信息。下面两张图将罗列出不同状态下的对象头信息
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.9version>
dependency>
public class User {
public static void main(String[] args) {
User user = new User();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}
内存地址 | 存储的数据(Byte) |
---|---|
0x00000000 | 0x12 |
0x00000001 | 0x34 |
0x00000002 | 0x56 |
0x00000003 | 0x78 |
大致意思就是这样,所以在大端模式下,最终取数据时(从低位开始取),于是完美还原12345678。小端模式的话,相反的。这里就不总结了。那么问题来了,我们如何知道我们的cpu是大端存储模式还是小端存储模式呢?java提供了如下api: | |
```java | |
// 输出结果参考如下内容: | |
// BIG_ENDIAN:大端模式 | |
// LITTLE_ENDIAN: 小端模式 | |
System.out.println(ByteOrder.nativeOrder().toString()); | |
``` |
public class Valid {
public static void main(String[] args) {
System.out.println(ByteOrder.nativeOrder().toString());
User user = new User();
System.out.println("before hashcode");
System.out.println(ClassLayout.parseInstance(user).toPrintable());
// 将hashcode转成16进制,因为jol在输出的内容中包含16进制的值
System.out.println(Integer.toHexString(user.hashCode()));
System.out.println("after hashcode");
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}
综上,咱们了解了java对象头的结构以及证明了无锁状态下的前56为存储的是hashcode。咱们要深刻理解java的对象头,这是理解synchronized关键字的基石。下篇文章主题为:证明分代年龄、无锁、偏向锁、轻量锁、重(chong)偏向、重(chong)轻量、重量锁
并发模块对应github地址:传送门
I am a slow walker, but I never walk backwards.