黑马程序员-Java 多线程(二)-线程的同步、死锁、Lock接口

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-

一、同步

在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)。
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不能参与执行。

需要同步的前提:必须要有两个或者两个以上的线程;必须是多个线程使用同一个锁;必须要保证同步中只有一次线程执行。

同步方式一共有两种:同步代码块和同步函数
- 同步函数: 即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

public class SynchronizedCounter {
    private int c = 0;
    public synchronized void increment() {
        c++;
    }
    public synchronized void decrement() {
        c--;
    }
    public synchronized int value() {
        return c;
    }
}

注意:
函数都有自己所属的对象this,所以同步函数所使用的锁就是this锁。
构造函数不能为同步的——在构造函数前使用synchronized关键字将导致语义错误。同步构造函数是没有意义的。这是因为只有创建该对象的线程才能调用其构造函数。

- 同步代码块:
 即有synchronized关键字修饰的语句块。 
 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
下面是一个实例:
class Ticket implements Runnable
{
    private int tick=100;
    Object obj = new Object();
    public void run()
    {
        while(true)
        {
            //给程序加同步,即锁
            synchronized(obj)
            {
                if(tick>0)
                {
                    try
                    {   
                        //使用线程中的sleep方法,模拟线程出现的安全问题
                        Thread.sleep(10);
                    }
                    catch (Exception e)
                    {
                    }
                    System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
                }
            }   
        }
    }
}

注意:上个代码里面同步代码块使用的锁可以是任意对象。
每一个对象都有一个与之相关联动的内部锁。按照传统的做法,当一个线程需要对一个对象的字段进行排他性访问并保持访问的一致性时,他必须在访问前先获取该对象的内部锁,然后才能访问之,最后释放该内部锁。在线程获取对象的内部锁到释放对象的内部锁的这段时间,我们说该线程拥有该对象的内部锁。只要有一个线程已经拥有了一个内部锁,其他线程就不能在拥有该锁了。其他线程将会在试图获取该锁的时候被阻塞了。

- 静态函数的同步方式

当同步函数被static修饰时,这时的同步用的是哪个锁呢?
静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,这个对象就是该类的字节码文件对象。
所以静态加载时,只有一个对象存在,那么静态同步函数就使用的这个对象。
这个对象就是 类名.class
最经典的例子便是懒汉式加同步 ,代码如下:

class Single{
    private static Single s = null;
    private Single(){}
    public static Single getInstance(){ 
        if(s == null){
            synchronized(Single.class){
                if(s == null)
                    s = new Single();
            }
        }
        return s;
    }
}

这样的做法,既保证了线程安全,又可以直接调用静态的对象。

二、死锁

死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。
当同步中嵌套同步时,就有可能出现死锁现象。下面就是个例子:

public class DeadlockRisk { 
    private static class Resource { 
        public int value; 
    } 

    private Resource resourceA = new Resource(); 
    private Resource resourceB = new Resource(); 

    public int read() { 
        synchronized (resourceA) { 
            synchronized (resourceB) { 
                return resourceB.value + resourceA.value; 
            } 
        } 
    } 

    public void write(int a, int b) { 
        synchronized (resourceB) { 
            synchronized (resourceA) { 
                resourceA.value = a; 
                resourceB.value = b; 
            } 
        } 
    } 
}

当有2个线程,1个线程在执行read()里synchronized (resourceA)时,另一个线程在执行write()里synchronized (resourceB),此时就会发生死锁,程序就死掉。有一些设计方法能帮助避免死锁,包括始终按照预定义的顺序获取锁这一策略。

三、线程通讯

线程的通讯就是运用(上一篇中)线程调度方法,是多个线程在操作同一个资源,但是操作的动作不同。下面是个实例:

public class ThreadA { 
    public static void main(String[] args) { 
        ThreadB b = new ThreadB(); 
        //启动计算线程 
        b.start(); 
        //线程A拥有b对象上的锁。线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者 
        synchronized (b) { 
            try { 
                System.out.println("等待对象b完成计算。。。"); 
                //当前线程A等待 
                b.wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println("b对象计算的总和是:" + b.total); 

    } 
} 

}public class ThreadB extends Thread { 
    int total; 
    public void run() { 
        synchronized (this) { 
            for (int i = 0; i < 101; i++) { 
                total += i; 
            } 
          notify();//线程A被唤醒 
        } 
    } 
}

要注意的是:对象上调用wait()方法时,执行该代码的线程会立即放弃它在对象上的锁。然而调用notify()时,并不这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程不会放弃锁直到完成后。

四、Lock接口

在JDK1.5中,专门提供了锁对象,利用锁可以方便的实现资源的封锁,这些内容主要集中在java.util.concurrent.locks包下面,里面有三个重要的接口Condition、Lock、ReadWriteLock。

Condition:Condition将Object监视器方法(wait、notify和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待 set(wait-set)。
Lock:Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
ReadWriteLock:ReadWriteLock维护了一对相关的锁定,一个用于只读操作,另一个用于写入操作。

下面是一个使用Lock接口的同步方法:
思路:设有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和 take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition 实例来做到这一点。
代码如下:

class BoundedBuffer {
   final Lock lock = new ReentrantLock();//创建一个锁对象
   final Condition notFull  = lock.newCondition(); //缓冲池信号量
   final Condition notEmpty = lock.newCondition(); //缓冲池信号量

   final Object[] items = new Object[100];
   int putptr, takeptr, count;
   public void put(Object x) throws InterruptedException {
     lock.lock();    //互斥信号量的锁
     try {   
       while (count == items.length)   //缓冲池满会进入等待状态
         notFull.await();
       items[putptr] = x; 
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();     //释放一个信号量
     } 
    finally {
       lock.unlock();
     }
   }
   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0) 
         notEmpty.await();
       Object x = items[takeptr]; 
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } 
finally {
       lock.unlock();
     }
   } 
 }

使用上面的接口方法同步,和锁的方法一样。所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。

五、总结

多线程的目的是为了最大限度的利用CPU资源。但是多线程的创建和停止会导致线程不安全情况。
因此通过线程的加锁或者使用同步代码块来进行。这两种操作实际都是对线程,通过notify()、notifyAll() 、wait() 方法来进行调度。
在JDK1.5更是用了专门的接口来实施上面的方法,使线程调度更加方便和安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值