synchronized 关键字 对某个对象加锁
public class T {
private int count = 0;
private final Object lock = new Object();
public void m() {
synchronized (lock) { // 任何线程要执行下面的代码,都必须先拿到lock锁,
//锁信息记录在堆内存对象中的,不是在栈引用中
// 如果lock已经被锁定,其他线程再进入时,就会进行阻塞等待
// 所以 synchronized 是互斥锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
// 当代码块执行完毕后,锁就会被释放,然后被其他线程获取
}
}
/**
* synchronized 关键字 对this加锁
* 每次使用锁都要newObject,比较麻烦,可以使用this代替object锁
*/
public class T {
private int count = 10;
public void m() {
synchronized (this) { // 任何线程要执行下面的代码,
//必须先拿到this锁
// synchronized 锁定的不是代码块,而是 this 对象
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
/**
* synchronized 关键字
* synchronized 方法
*/
public class T {
private int count = 10;
public synchronized void m() { // 等同于 synchronized (this) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
锁定静态方法,其实锁定的是 java.lang.Class 对象。
/**
* synchronized 关键字
* synchronized 静态方法
* 锁定静态方法,其实锁定的是 java.lang.Class 对象。 静态方法没有this引用的
*/
public class T {
private static int count = 10;
public static synchronized void m() { // 等同于 synchronized (T.class) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
public class T implements Runnable{
private int count = 10;
@Override
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T t = new T();
for (int i = 0; i < 5; i++) {
new Thread(t).start();
}
}
}
/*
某次运行结果:
Thread-0 count = 7
Thread-4 count = 5
Thread-3 count = 6
Thread-2 count = 7
Thread-1 count = 7
线程重入的问题(线程执行过程中,被其他线程打断),因为 count-- + " count = " + count 不是原子操作
解决办法,保证操作原子性,加上 synchronized 关键字
*/
同步方法和非同步方法是否可以同时调用?答:肯定可以
public class T {
private int count = 10;
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end");
}
public void m2() {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName()+" m2 running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m1).start();
new Thread(t::m2).start();
}
}
**
对业务写方法加锁,而对业务读方法不加锁,容易出现 脏读问题
因为,在执行写的过程中,因为读操作没有加锁,
所以读会读取到写未改完的脏数据
解决办法,给读写都加锁
**
public class Account {
String name; // 银行账户名称
double balance; // 银行账余额
public synchronized void set(String name, double balance) {
this.name = name;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public /*synchronized*/ double getBalance() {
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(() -> a.set("张三", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance()); // 0.0 double初始化的值0.0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance()); // 100.0
}
}
synchronized 是可重入锁. 即一个同步方法可以调用另外一个同步方法,
一个线程已经拥有某个对象的锁,再次申请时仍然会得到该对象的锁
public class T {
synchronized void m1() {
System.out.println("m1 start ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" m2"); // 这句话会打印,调用m2时,不会发生死锁
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m1).start();
}
}
synchronized 是可重入锁, 子类调用父类的同步方法,也是可重入的
下面例子中锁定的是同一个对象
public class T {
synchronized void m() {
System.out.println("m start ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end ");
}
public static void main(String[] args) {
TT tt = new TT();
tt.m();
}
}
class TT extends T {
@Override
synchronized void m() {
System.out.println(" child m start ");
super.m();
System.out.println(" child m end ");
}
}
synchronized 代码块中,如果发生异常,锁会被释放
在并发处理过程中,有异常要多加小心,不然可能发生数据不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问同一资源,
这时如果异常处理不合适, 第一个线程抛出异常,锁会内释放。
其他线程就会进入同步代码区,有可能访问到异常产生的数据,
第一个线程处理到一半的数据。
因此要非常小心处理同步业务员逻辑中的异常。
public class T {
int count = 0;
synchronized void m() {
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) { // 当count == 5 时,synchronized代码块会抛出异常
int i = 1 / 0;
}
}
}
public static void main(String[] args) {
T t = new T();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start(); // 执行到第5秒时,抛出 ArithmeticException
// 如果抛出异常后,t2 会继续执行,就代表t2拿到了锁,即t1在抛出异常后释放了锁
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
volatile 关键字,使一个变量在多个线程间可见
cn: 透明的,临时的
JMM(Java Memory Model):
在JMM中,所有对象以及信息都存放在主内存中(包含堆、栈)
而每个线程都有自己的独立空间,存储了需要用到的变量的副本,
线程对共享变量的操作,都会在自己的工作内存中进行,然后同步给主内存
下面的代码中,running 是位于堆内存中的t对象的
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,
在运行过程中直接使用这个copy,并不会每次都会去读取堆内存,
这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
使用volatile,将会强制所有线程都去堆内存中读取running的值。
加了volatile,不是线程每次都去主动去读,而是修改了值后,通知各个线程失效,重新读取
public class T {
/*volatile*/ boolean running = true; // 对比有无volatile的情况下,
//整个程序运行结果的区别
void m() {
System.out.println(" m start ");
while (running) { // 直到主线程将running设置为false,
//T线程才会退出
// 在while中加入一些语句,
//可见性问题可能就会消失,
//这是因为加入语句后,
//CPU可能就会出现空闲,
//然后就会同步主内存中的内容到工作内存
// 所以,可见性问题可能会消失
/*try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
System.out.println(" m end ");
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
volatile 关键字,使一个变量在多个线程间可见
volatile并不能保证多个线程共同修改running变量所带来的不一致的问题,
也就是说volatile不能替代synchronized,
即 volatile只能保证可见性,不能保证原子性
public class T {
volatile int count = 0;
/*AtomicInteger count = new AtomicInteger(0);*/
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++) {
count++;
/*count.incrementAndGet();*/
}
}
public static void main(String[] args) {
// 创建一个10个线程的list,执行任务皆是 m方法
T t = new T();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "t-" + i));
}
// 启动这10个线程
// threads.forEach(Thread::start);
threads.forEach((o)->o.start());
// join 到主线程,防止主线程先行结束
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 10个线程,每个线程执行10000次,结果应为 100000
System.out.println(t.count); // 所得结果并不为 100000,说明volatile 不保证原子性
}
}
/*
解决方案:
- 在方法上加上synchronized即可,synchronized既保证可见性,又保证原子性
- 使用AtomicInteger代替int(AtomicXXX 代表此类中的所有方法都是原子操作,并且可以保证可见性)
*/
synchronized 优化,同步代码块中的语句越少越好。比较m1和m2
public class T {
int count = 0;
synchronized void m1() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 业务逻辑中,只有下面这句代码需要 sync, 这时不应该给整个方法上锁
count++;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 业务逻辑中,只有下面这句需要 sync,这时不应该给整个方法上锁
// 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
synchronized (this) {
count++;
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
** 锁定某个对象o,如果o属性发生变化,不影响锁的使用
但是如果o变成另一个对象,则锁定的对象发生变化,
所以锁对象通常要设置为 final类型,保证引用不可以变**
public class T {
Object o = new Object();
void m() {
synchronized (o) {
while (true) {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "线程1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(t::m, "线程2");
t.o = new Object(); // 改变锁引用, 线程2也有机会运行,否则一直都是线程1 运行
thread2.start();
}
}
不要以字符串常量作为锁定对象,在下面的例子中, m1和m2其实是锁定的同一对象,这种情况下,还会可能与其他类库发生死锁,比如某类库中也锁定了字符串 “Hello”, 但是无法确认源码的具体位置,所以两个 “Hello” 将会造成死锁. 因为你的程序和你用的类库无意间使用了同一把锁。 原先jetty出现过类似的bug
public class T {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized (s1) {
}
}
void m2() {
synchronized (s2) {
}
}
实现一个容器,提供两个方法,add,size.写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到达5时,线程2给出提示并结束
public class MyContainer1 {
private List<Object> list = new ArrayList<>();
public void add(Object ele) {
list.add(ele);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainer1 container = new MyContainer1();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
container.add(new Object());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("add " + i);
}
}, "t1").start();
new Thread(() -> {
while (true) {
if (container.size() == 5) {
break;
}
}
System.out.println("监测到容器长度为5,线程2立即退出");
}, "t2").start();
}
}
此种方法是一种错误的实现:
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9
.... t2 一直在运行,永远不结束
这是因为 container 对象的可见性问题
public class MyContainer2 {
private volatile List<Object> list = new ArrayList<>();
public void add(Object ele) {
list.add(ele);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainer2 container = new MyContainer2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
container.add(new Object());
System.out.println("add " + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(() -> {
while (true) {
if (container.size() == 5) {
break;
}
}
System.out.println("监测到容器长度为5,线程2立即退出");
}, "t2").start();
}
}
添加 volatile ,使list发生变化时,主动通知其他线程,更新工作空间
上述代码,共有以下几个问题:
1. 不够精确,当container.size == 5 还未执行break时,有可能被其他线程抢占;
2. 或者 container.add() 之后,还未打印,就被 t2 判断size为5 直接退出了
3. 损耗性能,t2 线程,一直在走while循环,很浪费性能
这个程序一定要先启动t2线程
public class MyContainer3 {
private List<Object> list = new ArrayList<>();
public void add(Object ele) {
list.add(ele);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainer3 container = new MyContainer3();
final Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
System.out.println("t2 启动");
if (container.size() != 5) { //这里不需要while
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("监测到容器长度为5,线程2立即退出");
//t2自己结束之前,通知t1继续执行,t2结束了,锁也就释放了。
lock.notify();
}
}, "t2").start();
// 先启动t2线程,让t2线程进入等待状态
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
container.add(new Object());
System.out.println("add " + i);
// 当长度为5时,通知 t2 进行退出
if (container.size() == 5) {
lock.notify(); // notify 不会释放锁,即便通知t2,t2也获取不到锁
// 可以再wait一下,将锁释放,再让等待t2通知自己 t1继续执行
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
使用wait和notify , 必须锁定对象,wait和notify是调用被锁定对象的wait和notify。
xxx.wait() 当前线程释放锁,并且进入等待状态。别的线程可以进来。
xxx.notify() 只有其他线程调用这个对象的notify方法,
会唤醒正在这个对象上等待的某一个线程( CPU 随机唤醒)。
xxx.notifyAll方法,会唤醒这个对象上所有的等待线程。
wait()与notify() 方法的调用必须在同步代码块中
wait会释放锁,notify不会释放锁
锁定对象a,调用a.wait() 方法,当前线程就会进入等待状态,然后释放锁。
当某线程调用 a.notify() / a.notifyAll(), 叫醒在a对象等待的所有线程
使用CountDownLatch实现(最简单的方式)
Latch:门闩 使用Latch替代 wait notify来进行通信
好处是,通信简单,同时也可以指定等待时间
使用await和countDown 方法替代 wait 和 notify
CountDownLatch不涉及锁定,当count值为0时,当前线程继续运行
当不涉及同步,只涉及线程通信的时候,用synchronized + wait + notify 就显得太重了
public class MyContainer5 {
private volatile List<Object> list = new ArrayList<>();
public void add(Object ele) {
list.add(ele);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainer5 container = new MyContainer5();
// Count down 往下数 Latch 门闩
// 门闩不能保证可见性,不是一种同步方式,只是一种线程通信方式,保证不了可见性
// 门闩的等待,不会持有任何锁
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2 启动");
if (container.size() != 5) {
try {
latch.await();
// 指定等待时间
//latch.await(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("监测到容器长度为5,线程2立即退出");
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println("t1 启动");
for (int i = 0; i < 10; i++) {
container.add(new Object());
System.out.println("add " + i);
// 当长度为5时,撤掉一个门闩,此时门闩为0,门会打开,即t2会执行
if (container.size() == 5) {
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}