引入:
多线程提高了资源利用效率,但同时它也带来了线程安全的问题。比如在定电影票时,两个人都同时要定5排5座,或者是两个人同时给一张银行卡冲不同的钱,最后银行卡的钱是增加谁冲的呢 (其实就是说当两个线程同时去访问或改变一个资源时,线程是不安全的)?注意上面说的是同时,当然现实中不会出现那样的同时,因为同步方法已解决上面的问题了。
正解同步:
那什么是同步呢?这里千万不要误解!!!同步是协同步调,即在同一时刻,只能有一个线程访问临界资源,也就是同步互斥访问。怎么做到同步呢?通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
实现:
一: synchronizd关键字
1.同步代码块
语法:
synchronizd(同步锁)
{
需要同步操作的代码 //其实这段代码就是临界区
}
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块
一般把当前并发访问的共同资源作为同步监听对象.
// 同步代码块
public class SynchronizdDemo {
public static void main(String[] args) {
Apple1 a = new Apple1();
new Thread(a, "小A").start();
new Thread(a, "小B").start();
new Thread(a, "小C").start();
}
}
class Apple1 implements Runnable {
private int num = 50;
public void run() {
for (int i = 0; i < 50; i++) {
synchronized (this) {//this表示Apple1的对象,该对象属于多线程共享的资源
if (num > 0) {
// 模拟网络延迟
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "吃了编号为" + num + "的苹果");
num--;
}
}
}
}
}
2.同步方法
synchronizd 修饰的方法
synchronizd public void doWork()
{
方法体
}
对于非static方法,同步锁对象是this
对于static方法,使用当前方法所在类的字节码对象(Apple.class)
//使用同步方法
public class SynchronizdMethodDemo {
public static void main(String[] args) {
Apple2 a = new Apple2();
new Thread(a, "小A").start();
new Thread(a, "小B").start();
new Thread(a, "小C").start();
}
}
class Apple2 implements Runnable {
private int num = 50;
public void run() {
for (int i = 0; i < 50; i++) {
eat();
}
}
synchronized private void eat() {
if (num > 0) {
// 模拟网络延迟
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "吃了编号为" + num + "的苹果");
num--;
}
}
}
3.synchronizd (锁对象){
存在线程安全单位代码
}
锁对象:锁对象可以是任意对象,多个线程使用的必须是同一把锁,也就是说必须是同一个对象。锁对象必须是唯一的
锁对象被释放:同步代码块执行完毕/线程进入等待状态时/线程停止时
注意事项:
1.synchronized(锁住的对象), synchronized(this)锁住的只是对象本身,同一个类的不同对象调用的synchronized方法并不会被锁住,而synchronized(className.class)实现了全局锁的功能,所有这个类的对象调用这个方法都受到锁的影响,此外()中还可以添加一个具体的对象,实现给具体对象加锁。
2.不要用synchronizd修饰run方法,修饰之后,某一个线程就执行完了所有的功能,好比多个线程出现串行。
解决:把需要同步操作的代码定义在一个新的方法中,用synchronizd修饰该方法,再在run方法中调用此方法。
3.还有你有没有考虑过如果要把String类或基本类型作为对象监视器可以吗? 当然是不可以的,因为使用基本类型(JVM为它们维护了一个常量池)时会自动装箱得到新的值,即变量指向了新的对象,导致对象监视器前后不一致。
解决:设为常量,用 final 修饰,保证一直都是同一个对象监视器。
synchronized下的等待/通知机制的经典使用:生产消费者模型
//生产者与消费者互斥使用仓库
public static List<String> warehouse = new LinkedList<>();
public static void main(String[] args) {
//生产者线程
Thread thread_1 = new Thread(){
@Override
public void run() {
//对象监视器为warehouse,必须先获取这个对象监视器
synchronized ( warehouse) {
int i = 0;
//生产10个商品
while(warehouse.size()<=10){
++i;
//生产商品,添加进仓库
warehouse.add("生产了商品goods"+i);
//当商品数量足够时,便唤醒消费者线程
if(warehouse.size()>=10){
warehouse.notify();
//生产任务完成,跳出循环,结束运行,从而可以释放锁
break;
}
}
}
}
};
//消费者线程
Thread thread_2 = new Thread(){
@Override
public void run() {
synchronized (warehouse) {
try {//如果仓库的商品数量不能满足消费者
if(warehouse.size()<10){
//消费者进入等待队列,等待被唤醒
warehouse.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//消费商品
for(String goods:warehouse){
System.out.println("消费者消费了商品:"+goods);
}
}
}
};
//
thread_2.start();
thread_2.setPriority(Thread.MAX_PRIORITY);
thread_1.start();
}
二、显式锁 Lock
Lock相比与synchronized的优点:
Case 1 :
在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
Case 2 :
我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。
Case 3 :
我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。
因此也可想而知,Lock方式是需要手动获取锁和释放锁。