JAVA多线程之并发与同步,Lock与Syncronized

线程和进程的关系

进程是系统的执行单位,一般一个应用程序即是一个进程,程序启动时系统默认有一个主线程,即是UI线程,我们知道不能做耗时任务,否则ANR程序无响应。这时需要借助子线程实现,即多线程。由于线程是系统CPU的最小单位,用多线程其实就是为了更好的利用cpu的资源。


多线程创建方式

1、继承Thread类,重写run函数方法

class xx extends Thread{
    public void run(){
        Thread.sleep(1000);    //线程休眠1000毫秒,sleep使线程进入Block状态,并释放资源
    }
}
xx.start();    //启动线程,run函数运行

2、实现Runnable接口,重写run函数方法

Runnable run =new Runnable() {
    @Override
    public void run() {  
    }
}

3、实现Callable接口,重写call函数方法

Callable call =new Callable() {
    @Override
    public Object call() throws Exception {
        return null;
    }
}

  小结:Callable 与 Runnable 对比,它可返回值、可抛出异常

4、HandlerThread

通过handlerThread.quit()或者quitSafely()使线程结束自己的生命周期。

5、AsyncTask线程池

AsyncTask默认是开启一个线程队列,即含有5个新线程的线程池。详情可参考

6、IntentService

它是Service的子类,用法跟Service也差不多。耗时逻辑写在onHandleIntent(Intent intent)的方法体里。当任务执行完后会自动停止,无须手动去终止它。例如在APP里我们要实现一个下载功能,当退出页面后下载不会被中断,那么这时候IntentService就是一个不错的选择了。


线程管理

线程有六种状态:

  1. NEW (新建):线程尚未启动的线程状态。
  2. RUNNABLE (可运行):可运行线程的线程状态。
  3. BLOCKED (阻塞):一个线程的线程状态阻塞等待监视器锁定。
  4. TIMED_WAITING (计时等待):具有指定等待时间的等待线程的线程状态。
  5. WAITING (等待):等待线程的线程状态
  6. TERMINATED (结束):终止线程的线程状态。

1、wait():使一个线程处于等待状态,属于Object类中的。会释放持有的对象锁。

2、sleep():使一个线程处于休眠状态,属于Thread类中的。不会释放对象锁。让出cpu给其他线程,但是它仍然保持对Thread的监控状态,直到时间结束后恢复可运行状态。

3、notify():唤醒等待状态的线程。但是由JVM决定且不按优先级。

4、allnotify():唤醒所有等待状态的线程。并不是给所有线程上锁,而是让它们竞争。

5、join():挂起(插队)等待该线程结束,才能执行其他线程,属于Thread类中的。

6、yield():礼让,Thread类中的yield方法可以让一个running状态的线程转入runnable。


基础概念

1、并行。多个cpu实例或多台机器同时执行一段代码,是真正的同时。
2、并发。让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。
3、线程安全。多线程并发情况下,线程的调度顺序不影响最终结果。线程不安全就意味着线程的调度顺序会影响最终结果,比如某段代码不加事务去并发访问。
4、线程同步。通过控制和调度保证共享资源的多线程访问成为线程安全,来保证结果的准确。如某段代码加入@synchronized关键字。
5、原子性。一个操作或者一系列操作,要么全部执行要么全部不执行。数据库中的“事物”就是个典型的原子操作。
6、可见性。当一个线程修改了共享属性的值,其它线程能立刻看到共享属性值的更改。如JMM分为主存和工作内存,共享属性的修改过程是在主存中读取并复制到工作内存中,在工作内存中修改完成之后,再刷新主存中的值。若线程A在工作内存中修改完成但还没来得及刷新主存中的值,这时线程B访问该属性的值仍是旧值。这样可见性就没法保证。
7、有序性。程序的执行顺序会按照代码的先后顺序执行。


Synchronized 同步锁

synchronized可以修饰普通方法、代码块、静态方法(锁类)。 它可以保证只有一个线程拿到锁访问共享资源。

  • 悲观锁:每次访问共享资源时都会上锁。
  • 非公平锁:线程获取锁的顺序并不一定是按照线程阻塞的顺序。
  • 可重入锁:已经获取锁的线程可以再次获取锁。(底层有count变量计数)
  • 互斥/排他锁:该锁只能被一个线程所持有,其他线程均被阻塞。

Synchronized 锁升级理论

偏向锁:只有一个线程争抢锁资源时,将线程拥有者标识为当前线程;默认开启)

轻量级锁(自旋锁):多个线程数竞争锁时开启,即多线程通过CAS去争抢锁,抢不到则自旋。

轻量级锁就适用于耗时任务较短线程,少量自旋,就能够轻松获取到锁。若A持用锁迟迟不释放,其他线程则循环尝试获取锁,获取不到就一直自旋,这个过程对CPU资源是有损耗的。所以,就将轻量级锁锁升级为重量级锁。

重量级锁:多个线程竞争锁时,将未争抢成功的锁放到队列中阻塞;

轻量级锁升级重量级锁的时机:当自旋的线程循环超过10次,或者线程等待的数量超过cpu的1/2,就会将轻量级锁升级为重量级锁。


Synchronized 原理

在jdk1.6之前synchronized只能实现重量级锁,之后优化自旋锁/轻量级锁、偏向锁等。

Java虚拟机是基于Monitor 对象监视器来实现锁的。

Monitor是由ObjectMonitor实现的,其源码是用C++语言编写的,Hotspot的源码下载链接:http://hg.openjdk.java.net/jdk8/jdk8/hotspot。找到ObjectMonitor.hpp文件,路径是src/share/vm/runtime/objectMonitor.hpp,其数据结构:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //锁的计数器,获取锁时值加1,释放锁值减1
    _waiters      = 0,  //等待线程数
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;  // 存储Monitor对象
    _owner        = NULL;  // 持有当前线程的owner
    _WaitSet      = NULL;  // wait状态的线程列表
    _EntryList    = NULL ;  // 处于等待锁状态block状态的线程列表
       .....}

其中 _owner、_WaitSet和_EntryList 字段比较重要,它们之间的转换关系如下图 

同步机制 

  • 新线程先尝试获取锁,失败后进入到等待列表进行block;
  • 阻塞列表中线程通过CAS的方式,尝试获取owner;(互斥锁)
  • 若获取锁成功, count +1;且 owner  == 当前线程,则 recursions+1;(重入锁)
  • 若获取锁失败,则返回到EntryList中。
  • 线程释放锁,count -1,recursions-1;当 recursions = 0时则已经释放了锁。

_WaitSet

持有锁的线程调用wait()方法,owner = null,count -1,recursions-1,当前线程加入到WaitSet中,等待被唤醒。


Lock 同步锁

Lock是个接口,实现类ReentrantLock重入锁也叫做递归锁。

所谓重入锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码。如果线程可以继续持有这个锁,那么就是可重入的。否则就是不可重入锁。

重入锁,可以解决避免死锁问题,Synchronized也可重入锁。

使用示例:

        class Bank {
            private int account = 100;
            private Lock lock = new ReentrantLock(); //需要声明这个锁
            public int getAccount() {
                return account;
            }
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }
            }
        }

用ReentrantLock时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

Lock 支持公平锁 与非公平锁 

  • 公平锁:在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列。如果是等待队列的第一个就占有锁,否则就加入等待队列中。以后会按照FIFO的规则从队列中取到自己。
  • 非公平锁:上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

非公平锁性能高于公平锁。非公平锁能更充分的利用cpu的时间片,减少cpu空闲的状态时间。

底层实现,通过AQS。详情可查看:JAVA 并发编程之AQS排队同步框架 


Lock 与 Syncronized 

  • Lock 显式的加锁lock() 和unlock()解锁。不同的是 synchronized 的加解锁是隐式的,尤其是抛异常的时候也能保证释放锁。
  • Lock 可以反向解锁。比如先Lock1锁再Lock2 锁,可以先解锁 Lock1,再解锁 Lock2。Synchronized 只能先对 obj2 解锁再 解锁 obj1。因为 synchronized 加解锁是由 JVM 实现的,在执行完代码后会自动解锁。所以会按照 synchronized 的嵌套顺序加解锁,不能自行控制。
  • Synchronized 重量级,若每个线程持有锁很长时间,那么整个程序的运行效率就会降低。Lock 类的线程在等锁的过程中,可以中断退出,也可以用 tryLock() 方法主动尝试获取锁
  • Lock支持并发读锁, Synchronized 做不到。
  • Lock支持公平锁与非公平锁 ,Synchronized只支持非公平锁。
性能区别

        在 Java 5 以及之前,synchronized 的性能比较低,但是到了 Java 6 以后,发生了变化,因为 JDK 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差

如何选择

如果 synchronized 关键字适合你的程序, 那么请尽量使用它。

synchronized 加解锁是由 JVM 实现,可以减少编写代码的数量,减少出错的概率。

若干lock 忘记在 finally 里 unlock,就会出现死锁。程序就会出问题,synchronized 更安全。

如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用 Lock。


推荐文章:

JAVA 线程优化及线程池管理

Java CAS原子操作过程及ABA问题

JAVA 并发编程之AQS排队同步框架

Java 集合知识:List、HashMap、Set

  • 22
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

艾阳Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值