线程同步的方法有哪几种_实现线程的几种方式

线程同步的方法有哪几种_实现线程的几种方式Java多线程我们通常说的保持同步,其实就是对共享资源的保护。在单线程模型中, 我们永远不用担心“多个线程试图同时使用同一个资源的问题”, 但是有了并发, 就有可能发生多个线程竞争同一个共享资源的问题。就好比

Java多线程   我们通常说的保持同步,其实就是对共享资源的保护。在单线程模型中, 我们永远不用担心“多个线程试图同时使用同一个资源的问题”, 但是有了并发, 就有可能发生多个线程竞争同一个共享资源的问题。   就好比你正在餐厅里吃饭,当你拿起筷子正要夹盘子里的最后一块肉时, 这片肉突然消失了。因为你的线程被挂起了, 另一个人进入餐厅并吃掉了它。   这就是我们在多线程下需要处理的问题—-我们需要某种方式来防止两个任务同时访问相同的资源   那么我们很容易想到第一种方法: 加锁, 好比我们进入卫生间之后要把门关上, 下一个人来到卫生间门口要先敲门,没人的话他就可以直接使用, 否则就要等到里面的人出来。不管你多么着急,不管外面排了多少人,没办法,只要他还在里面,那你就只能等,哪怕他在里面睡觉玩手机。。。 当他终于打开门出来的时候, 离门最近的那么人很有可能会成功进入, 但这一点并不能保证。   同理, 我们给共享资源加一把锁,任意时刻都只允许一个线程操作共享资源,当某个线程试图访问该资源时,需要先检查锁的状态,如果当前没有其他线程在使用, 则锁,开始操作资源,操作完成后再释放资源;否则就要排队等待。   常见的加锁方法大致有以下几种:   1, synchronized关键字修饰方法   之前在对HashMap的描述中, 我们说hsahMap是线程不安全的, 但是古老的Hashtable是线程安全的, 就是因为HashTable中对所有操作共享资源的方法都使用了synchronized关键字进行了修饰, 如下:   
线程同步的方法有哪几种_实现线程的几种方式        共享资源一般是以对象形式存在的内存片段,也有可能是文件,输入/输出端口,打印机等。但是要控制对共享资源的访问, 得先把它包装进一个对象,然后把所有要访问这个资源的方法标记为synchronized.    注意, 这里的描述是“所有”。 为什么呢?   因为java中的对象都自动含有单一的锁(也叫监视器或者对象锁), 当某个线程在对象上调用其任意synchronized方法时,此对象会被加锁(锁住的是整个对象),所以在该线程释放锁之前, 其他线程无法访问该对象内任一修饰为synchronized的方法。(其他未被synchronized修饰的方法可以被随意访问)   一个线程可以多次获得对象的锁,JVM负责跟踪对象被加锁的次数,如果锁被释放,则重置为0, 在线程第一次给对象加锁的时候,计数为1,每当这个相同的线程在对象上获得锁时(从一个synchronized方法到另一个synchronized方法),计数+1,显然只有首先获得锁的线程才可以继续获得多个锁。每离开一个synchronized方法, 计数-1。当计数为0时,锁完全释放   注意: 当我们使用synchronized保护共享资源时, 记得声明该资源为private, 防止其他线程直接访问域   java中的类也有一个锁,作为类的class对象的一部分。所以当我们用synchronized关键字修饰一个静态方法时,该方法的锁也意味着整个类中的static方法都会被加锁。   2, synchronized关键字锁定代码块   上文中我们说到,古老的hashtable是线程安全的,因为它在源码中对所有操作共享资源的方法都加了锁。   但是我们在日常开发中,很少会用到它。因为在某些情况下,我们其实只需要保护方法里的核心代码,为整个方法加锁会增加多线程访问下的时间成本   大家可以回顾一下单例模式中的双重锁模式   
线程同步的方法有哪几种_实现线程的几种方式        为什么要判空两次?然后在第二次判空之后才加锁呢?   如果我们直接给getSingleTon方法加锁,当然也能实现同步。但带来的问题是, 如果singleTon已经被创建,应该直接返回就好了,但事实上每次线程执行到这里都要试图锁,这是不必要的开销。   所以第一层判断如果singleTon已经被创建,则无需锁直接返回。   第二层判断的意义是,想象一下,第一个线程来到这里,检查发现singleTon为空,那么它就会获得锁, 并创建了一个singleTon的实例。在它创建singleTon实例的过程中,另一个线程也来到了这里执行了第一层判断,发现singleTon为null(因为此时第一个线程还没有完成创建), 于是它排队等待,然后第一个线程创建完成之后释放锁,第二个线程进入同步块,此时如果没有第二层判空,那么它就会直接创建一个singleTon实例, 这样就有了两个实例   值得一提的是, 当我们使用synchronized同步代码块时, 需要传入一个类或者说对象,如上文中我们传入了SingleTon.class   因为synchronized快必须指定一个在其上进行同步的对象,通常最合理的方式是使用使用其方法正在被调用的当前对象,如   synchronized(this)   当我们传入this时, 如果某个线程获得了同步块中的锁, 那么当前对象中其他的synchronized方法和synchronized块都不能被其他线程调用(其实跟上文说到对象锁的一样)   当然, 如果需要的话,我们也可以在在一个对象的同步块中去同步另一个对象, 比如我们在A对象的某个方法中 synchronized(B), 那么当某个线程获得锁时, 它获得的是B的对象锁, 这也就意味着与此同时B中的synchronized方法/块不能被其他线程执行, 而A中的则不受影响。       3, ReentrantLock显式加锁    java.util.concurrent类库中还包含了定义在java.util.concurrent.locks中的显式互斥机制,如ReentrantLock, 它的简单用法如下:   
线程同步的方法有哪几种_实现线程的几种方式   
线程同步的方法有哪几种_实现线程的几种方式        可以看到, 我们使用lock时,必须显式地创建,锁定和释放,所以与synchronized相比,代码缺乏优雅性。但是,对于解决某些类型的问题来说,它更加灵活。   注意, 我们应当使用 try-finally语句, 确保在finally中unlock。 如果该方法有返回值, return语句应放在try中, 以确保unlock不会过早发生   当我们使用synchronized关键字时, 某些事物失败了就会抛出异常, 但我们没有机会去做一些清理的工作。 显式lock的优点就体现在这里, 我们可以在finally子句中,将系统维护在正确的状态。   4,ReentrantReadWriteLock显式加锁   ReentrantReadWriteLock实现了ReadWriteLock接口,这种锁的特点是,允许同时有多个读取者,只要它们都不试图写入就行。如果写锁已经被其他线程持有, 那么任何读取者都不能访问,直到写锁被释放。   所以,针对那些频繁读取,极少写入的情况, 使用ReentrantReadWriteLock可以提高性能。   ReentrantReadWriteLock的用法大致如下:没有仔细研究过,不保证正确   
线程同步的方法有哪几种_实现线程的几种方式           5, 使用ThreadLocal进行线程本地存储   上文说到的4种方式从本质上来说其实都是一样的, 都是通过对共享资源加锁的方式来实现同步   接下来我们保护共享资源的的另一个解决思路——根除对变量的共享   线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程创建不同的存储, 通常写法如下:   
线程同步的方法有哪几种_实现线程的几种方式        注意: ThreadLocal对象通常当作静态域存储,在创建ThreadLocal时,只能通过get和set方法来访问该对象的内容。   其中value.get()方法将返回与当前线程相关联的对象的副本, 而set会将参数插入到为其线程存储的对象中, 并返回存储中原有的对象       6, 利用可见性实现同步– volatile   volatile关键字确保了应用中的可视性,如果你将一个域声明为volatile, 那么只要对这个域产生了写操作,那么所有的读操作都可以看到修改,即使用了本地缓存,情况也是如此,因为volatile域会立即被写入到主存中,而读取操作就发生在主存中。   volatile是轻量级的synchronized, 它比 synchronized执行成本低因为它不需要切换上下文以及调度线程。   但是, volatile只适用于静态域,就是说只有一个线程对共享资源进行写操作,可以有多个线程执行读操作。当一个域的值依赖于它之前的值(如递增一个计数器)或者这个域的值受其他域的值限制时,volatile将无法工作。   同时volatile关键字并不保证原子性       7, 使用原子性操作(原子类)来保证同步   在java中,原子的意思就是不可再分,比如 return i 我们可以认为它是原子性的, 但 i++并不是原子性的   原子操作可有线程机制来保证其不可中断。一旦操作开始, 那么它一定可以在可能发生的线程切换之前被执行完毕。   因此,如果我们确定某个操作时原子性的, 那它就是线程同步的。   所以,在某些情况下,我们可以使用原子类来保证同步。如AtomicInteger, AtomicLong, AtomicReference等   这些类是在机器级别上的原子性,因此使用他们的时候,通常不用担心同步问题。       8, 使用SingleThreadExecutor   在上一篇文章–启动线程 中我们提到过,SingleThreadExecutor的调用会产生单线程执行器, 当我们set多个线程时, 她们将按照被提交的顺序依次执行       9, 免锁容器。 如Vector, Hashtable这些早期容易,使用了大量的synchronized方法来保证同步   Java SE5特别添加了一些新的容器,如CopyOnWriteArrayList, CopyOnWriteArraySet, ConcurrentHashMap   这类免锁容器背后的通用策略是: 对容器的修改可以与读取操作同时发生, 只要读取者能看到修改后的内容就行。   修改是在容器数据结构的某个副本中执行的, 并且这个副本在修改过程中不可视,修改完成后,会立即将修改后的数据与主数据结构进行交换。   值得特别说明的是, ConcurrentHashMap还引入了分段锁,将数据分成多个数据段分别加锁,从而提高并发性能。            小结:   其实从根本上来说,上面的1,2,3,4都可归纳为加锁的方式,所以   一, 加锁 (上面的1,2, 3,4)   二,线程封闭,消除对资源的共享(5)   三, 利用可见性(6)   四,原子类(7)   五,executor框架(8)   六, 使用同步类/免锁容器(9)    

2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/44537.html

(0)
上一篇 2024年 9月 5日 下午11:43
下一篇 2024年 9月 5日 下午11:51

相关推荐

关注微信