一、synchronized 关键字
1、synchronized 用法
synchronized 关键字可以保证线程的原子性和可见性,它锁的是一个对象,他有如下几种用法
1)对象锁
这里锁的是 object 对象的实例。
public class Demo1 {
private int count = 10;
private Object object = new Object();
public void test(){
synchronized (object) {
while (count>0) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
public static void main(String[] args){
Demo1 demo1 = new Demo1();
new Thread(()->demo1.test(),"t1").start();
new Thread(()->demo1.test(),"t2").start();
}
}
2)this锁
这里使用的是 this 锁,锁的是当前类的实例。
public class Demo2 {
private int count = 10;
public void test(){
synchronized (this) {
while (count > 0) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
public static void main(String[] args){
Demo2 demo2 = new Demo2();
new Thread(()->demo2.test(),"t1").start();
new Thread(()->demo2.test(),"t2").start();
}
}
3)非静态同步函数
直接声明在方法上,相当于相当于是 synchronized(this)
public class Demo3 {
private int count = 10;
public synchronized void test(){
while (count>0) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
public static void main(String[] args){
Demo3 demo3 = new Demo3();
new Thread(()->demo3.test(),"t1").start();
new Thread(()->demo3.test(),"t2").start();
}
}
4)静态同步函数
1)synchronized 关键字修饰的静态方法锁的是类的.class 文件;
2)静态方法中,锁定的对象只能是类的.class文件,不能是类的实例;
public class Demo4 {
private static int count = 10;
//静态方法中synchronize锁定代码块,锁定的对象不能是类的实例,只能是类的.class文件。
public synchronized static void test(){
while (count>0) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
public static void test2(){
//这里不能替换成this
synchronized (Demo4.class){
while (count>0) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
public static void main(String[] args){
Demo4 demo4 = new Demo4();
new Thread(()->demo4.test2(),"t1").start();
new Thread(()->demo4.test2(),"t2").start();
}
}
2、使用 synchronized 关键字注意事项
1)防止锁对象的引用的改变
当我们定义了一个对象锁,锁住了这个对象 o,如果这个对象的属性发生改变,不会影响锁住的对象,如果这个对象变成了一个新的对象,也就是重新 new 了一次,那锁住的对象就会发生改变,如下例子。
public class Demo1 {
Object o = new Object();
public void test(){
synchronized (o) {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
Demo1 demo = new Demo1();
new Thread(demo :: test, "t1").start();
Thread t2 = new Thread(demo :: test, "t2");
//去掉此句 t2 无法执行,否则 t2 能够执行
demo.o = new Object();
t2.start();
}
}
2)不要用字符串常量作为锁的对象
如果使用字符串常量作为锁,在字符串相同的情况下,锁住的则是同一个对象,因为他们的引用都指向常量池的同一个地址。
public class Demo2 {
String s1 = "hello";
String s2 = "hello";
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) {
Demo2 demo = new Demo2();
new Thread(demo :: test1,"test1").start();
new Thread(demo :: test2,"test2").start();
}
}
3)同步代码块中的语句越少越好
在 test1 中,是给整个方法上了锁,但由于我们的业务逻辑只需要锁住的对象是count++,所以在 test2 中我们只给了这个代码上了锁,这样细化了锁的粒度,使代码的效率得以提高。
public class Demo3 {
int count = 0;
public synchronized void test1(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
count ++;
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3、 synchronized 关键字特点
1)同步方法和非同步方法可以同时调用
public class Demo{
public synchronized void test1(){
System.out.println(Thread.currentThread().getName() + " test1 start...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " test1 end...");
}
public void test2(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " test2");
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(demo :: test1,"test1").start();
new Thread(demo :: test2,"test2").start();
}
}
2)synchronized支持重入
synchronized支持重入,一个同步方法调用另一个同步方法,也可以得到锁。
public class Demo {
synchronized void test1(){
System.out.println("test1 start.........");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test2();
}
synchronized void test2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test2 start.......");
}
public static void main(String[] args) {
Demo demo= new Demo();
demo.test1();
}
}
3)异常情况会自动释放锁
如果碰到异常情况,当先线程会自动释放锁
public class Demo {
int count = 0;
synchronized void test(){
System.out.println(Thread.currentThread().getName() + " start......");
while (true) {
count ++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
//碰到异常的情况,如果没有处理,会自动释放锁,所以T2可以执行。
int i = 1/0;
}
}
}
public static void main(String[] args) {
Demo demo11 = new Demo();
Runnable r = () -> demo11.test();
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
二、volatile 关键字
1、volatile 关键字特点
1)保证可见性
什么是可见性,在JMM(Java 内存模型) 中分为主存和本地私有内存,主存中存放的是所有线程的共享数据,本地内存中存放的是某个线程的私有数据,在某一线程启动时,会先去读取主内存的数据到本地私有内存,然后再运行这个副本,但并不会每次都去读取,所以会造成数据不一致问题。
volatile 关键字正是为了解决这个问题,我们通过以下代码举例。
以下代码,在不添加 volatile 关键字前,虽然 running 改变了,但线程 t1 仍然不会停止,是因为虽然主内存改变了,但本地私有内存并不知道。 当添加了 volatile 关键字后,每一次被 volatile 关键字修饰的变量的值发生改变时,主内存都会通知本地私有内存进行修改,所以线程 t1 可以结束。
public class Demo {
/*volatile*/ boolean running = true;
public void test(){
System.out.println("test start...");
while (running){
}
System.out.println("test end...");
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(demo :: test,"t1").start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.running = false;
}
}
2)不保证原子性
如下代码,多个线程对一个共享变量进行累加,比如一个线程 A 加到了100,但是还没有往上添加,另一个线程 B 也来了,它也把 100 拿过来进行累加,他们两都加了 1 ,于是把结果101 存了进去,虽然是加了 2 次,但实际上只加了 1 次,所以 volatile关键字不保证原子性,它不能代替Synchronized。
public class Demo {
volatile int count = 0;
public void test(){
for (int i = 0; i < 10000; i++) {
count ++;
}
}
public static void main(String[] args) {
Demo demo = new Demo();
List<Thread> threads = new ArrayList();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(demo::test, "thread-" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println(demo.count);
}
}
当我们在方法上添加了 synchronized ,发现最终结果为 10000,说明 synchronized 既保证了可见性,又保证了原子性。
public class Demo {
int count = 0;
public synchronized void test(){
for (int i = 0; i < 10000; i++) {
count ++;
}
}
public static void main(String[] args) {
Demo demo = new Demo();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(demo::test, "thread-" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println(demo.count);
}
}
我的 Github:Github
CSDN: CSDN
个人网站: sirius 的博客
E-mail: [email protected]