关于多线程和线程安全相关讨论(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bloodylzy/article/details/79140479

线程安全的数据结构:

 

1.线程安全:

线程安全问题说的直白一点就是多个线程在操作同一个对象的时候,会不会出现问题。我们时常说的,StringBuidle,HashMap,ArrayList这些线程不安全,就是在说多个线程在操作这个对象的时候,可能会出现数据不正确的问题。

所以说,所谓的线程安全都是指一个公共对象被多个线程操作,至于你写在run方法里面的局部变量,那根本和线程安全无关,所以说,多多创建局部变量

为什么StringBuffer,HashTable,Vector这些东西是线程安全的呢。下面是我查找资料后,得出的总结,如果不对,还请大家指出。

我们多个线程操作同一个对象—姑且称这个对象为a—基本上是这么一个操作:a这个对象存在于线程公共内存空间(也就是Java堆)当中,但是每个线程在运行的时候,都是有自己的一个线程工作内存空间(从Java栈里面分配出去的)。所以多线程去操作对象a的时候,一般会分成三步:

1.将线程公共内存空间中的对象a,写入到自己的线程工作内存空间当中。

2.修改自己线程工作内存空间当中的对象a

3.将自己线程工作内存空间中的对象a写入到线程公共内存空间当中。

另外,Java是支持多线程的语言,而一颗CPU只能执行一个线程,但是我们在使用多线程的时候,总不会说是根据服务器CPU的核数来确定你的线程数,而且另外其他程序的线程也在霸占着你的CPU。所以当线程数大于你的CPU核数的时候,线程之间会根据时间片轮询来抢夺CPU资源(不要问我什么叫时间片轮询)。所以也就是说,可能会存在着一个线程运行到一半的时候,因为CPU的调度,导致线程暂停的情况。

接下来就是网上老掉牙的,关于ArrayList线程不安全的举例。现在两个线程AB,他们都要给公共内存中空的ArrayList赋值1。然后,A完成了前面的两个步骤,但是因为CPU调度,线程A暂停了,接着线程B开始执行了,当线程B开始执行完前两个步骤的时候,A又可以继续了,这时候,线程A和线程B同时往ArrayList赋值1,这就导致了ArrayListsize1,值是2

但是如果改成Vector为什么又可以了呢

因为线程安全的数据结构再被多个线程操作的时候,会多两个步骤加锁和解锁。

继续上面的例子,当程序运行到最后一步,AB同时要对公共对象赋值的时候,会发生不同。因为Vectoradd()上面有锁的概念。哪一个线程抢先一步开始写入,那么这个线程就拥有这个公共对象的锁,而没有得到锁的线程只能等待,等到对象的锁被解开的时候,才可以继续写入。这就保证了同一时间只有一个线程对公共对象进行写的操作


2.并发的数据结构:

本文注重实用性,所以上面那个看上去很复杂的原子操作我们就不说了,下面是推荐使用的并发数据结构

1.并发List

线程安全的List主要是有两种,一个是我们之前提到过的Vector,还有另外一个ListCopyOnWriteArrayList。虽然两个都是线程安全的,但是效率是不一样的。Vector是读写都是上锁的,但是CopyOnWriteArrayList这个结构,它的读取是不上锁的,而它的写入是加入重入锁。具体可以看一看这两个结构。总的来说,如果是注重读取的话,建议使用CopyOnWriteArrayList,但是如果侧重写入的话,VectorCopyOnWriteArrayList要好

2.并发Map

一般我们使用的Map结构多是HashMap,但是HashMap是线程不安全的,用于被多线程共同编辑可能会出错。可能会有人说HashTable,但是这里推荐大家使用concurrentHashMap。具体大家可以去看这两个类的具体实现。这里简单提一下,对于HashTable而言,这个类太线程安全了,size()put(),get()这些方法全是加上这个synchronized关键字,但是大家可以看到,concurrentHashMap这个类,只有在put()这种写操作上面是上锁的,而且还不是整个方法上锁,是对代码块进行上锁,锁的持有时间更断,所以在高并发的情况下更好。

3.并发Queue

并发队列上面,JDK提供了两个主要的,一个是concurrentLinkedQueue,另一个BlockingQueue接口。前者主要是负责在高并发的前提下,仍然能够有一个良好的队列性能;后者则是在于简化多线程间的数据的共享,说的直白一点,多用于生产者和消费者的情境下。

相比于concurrentLinkedQueueoffer()poll()而言,BlockingQueue加入了阻塞的机制,我个人更倾向于使用put()take()put()方法是offer()的一种延伸,在队列没有足够的空间去加入任务之后,会阻断线程,直到队列有空间;另一方面,take()方法是poll()的一种延伸,在队列中没有任务的时候,线程会被阻断,直到队列有新的任务;

上面也说了,BlockingQueue是一个接口,具体的实现有ArrayBlockingQueueLinkedBlockingQueue两种。这两个我们在上面的最基本的线程池的讲解中有讲到过。前者是基于数组的阻塞队列,所以是一个定长数组;而LinkedBlockingQueue是基于链表的阻塞队列,他默认是无限制的,当然你也可以选择有参的构造方法(参数标明链表长度)

总结而言:BlockingQueue接口最常用的应用场景是多线程间的数据共享,如果需要高性能的队列,可以使用concurrentLinkedQueue

3.关于同步字 :

同步关键字synchronized大家都很熟悉,一般来说用同步关键字去修饰方法,基本上都可以解决同步问题

但是需要明确的几个问题:
  1)synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
  2)无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
  3)每个对象只有一个锁(lock)与之相关联。
  4)实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
  1、synchronized关键字的作用域有二种:
  1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
  2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
  synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。
  在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问
  synchronized 方法的缺陷:同步方法,这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象 P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法.同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:
  2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象。



这里最后提到一个ThreadLocal这个局部变量的概念,具体可以参考MyThreadLocal和MyThreadLocalMain这两个文件。我们可以看到,这个变量虽然是被定义成static,同时也没有被上锁,但是却完全没有发生多个线程之间数据相互影响的事故。

具体的原因大家可以参考查看一下ThreadLocal的get()方法

猜你喜欢

转载自blog.csdn.net/bloodylzy/article/details/79140479