java多线程

来源:互联网 发布:预测走势 算法 编辑:程序博客网 时间:2024/05/02 13:03

多线程的优点:
资源利用率更好
程序设计在某些条件下更简单
程序响应更快

一 .创建线程的2种方法:
1.1 第一种:继承Thread类,并覆盖其run方法,利用new创建对象(对象必须是Thread或者Thread的子类才能创建线程)来调用run方法(创建一个线程),开启线程使用start方法(本质是去调用Runnable接口中run方法)
这里写图片描述
如果上图中threadTest1.run(),也能执行,执行结果就是先执行ThreadTest1 is run 60次后,才开始执行main is run60次。原因是threadTest1.run()执行的是普通的方法调用,而没有开启线程,主线程从上向下执行,所以先执行ThreadTest1 is run 60次后,才开始往下执行main is run(这样就是单线程执行)
1.1 第二种:实现Runnable接口,这种也是最常用的方式
必须有以下几步骤:
1,实现Runnable接口,并复写run的无参方法(这里存放线程执行的代码)
2,创建Runnable接口子类的对象
3,创建Thread类的线程对象,并把Runnable接口子类的对象作为参数传给Thread的构造函数
4,Thread类的线程对象调用start方法,开启线程,并调用Runnable接口子类的run方法。
总结:这二种创建线程的方式的区别?
实现方式(实现Runnable接口)可以避免单继承(已经继承了别的类就不能继承Thread类了)创建线程的局限性,在定义线程时,建议使用此方式
二种方式的区别:继承Thread:线程代码存放在Thread子类run方法中。 实现Runnable接口,线程代码存放在接口的子类的run方法中
这里写图片描述
1.2、线程的运行状态
这里写图片描述
上图中的阻塞状态,当线程被冻结后sleep时间到,或者notify()唤醒了,具备执行权,也可能立即执行也可能处在阻塞状态,等待cpu的调用
1.3、获取线程对象及名称
Thread.currentThread():获取当前线程的对象
getName():获取线程名称。
设置线程名称:setName或者使用构造函数
1.4、多线程的安全问题(重点理解)
这里写图片描述
1.5、同步函数,把synchronized放在方法前作为修饰符,同步函数的锁是this,如果二个线程,一个在同步代码块,一个在同步函数中,都执行共享数据时,他们要用同一把锁才可以避免安全问题(同步函数的锁为this,所有同步代码块也必须为this锁)
这里写图片描述
静态同步函数使用的是什么锁呢,通过验证不是this,也不可能是this(静态方法中不可以定义this,静态进内存时,内存中没有本来对象,但是有该类的字节码文件对象 (类名.class,该对象的类型是Class))
综上所述:静态的同步方法,使用的锁是该方法所在类的字节码文件对象(类名.class)
1.6、根据多线程的扩展(单例设计模式懒汉式和饿汉式)
饿汉式单例代码如下

class Single{    private static final Single s=new Single();    //私有化构造函数    private static Single(){}    //对外提供一个访问的方法    public Single getInstrance(){           return s;    }}

这个不涉及多线程安全问题。但是懒汉式就会存在多线程安全问题,而且面试的时候很爱考察
这里写图片描述
1.7、多线程的死锁
同步中嵌套同步,而且锁还不一样,就会出现死锁
1.8、守护线程(也称为后台线程)setDaemon(),该方法必须在线程启动前调用。
1.9、join方法:当A线程执行到B线程的join()方法时,A就会等待,等B线程执行完,A才会执行
join通常用来临时加入线程执行

后来经过看了张老师的视频后,代码写得真是经典

这里写图片描述
经过分析Thread类的源码,理解的更深了,Thread类实现了Runnable接口
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

二、java 5.0的新特性 线程并发库,以下的三个包都是5.0后增加的操作线程
(1)java.util.concurrent 线程并发工具类
(2)java.util.concurrent.atomic 类的小工具包,支持在单个变量上解除锁定的线程安全编程。
此包下边的类
1、AtomicIntegerFieldUpdater 基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。
2、AtomicLongFieldUpdater 基于反射的实用工具,可以对指定类的指定volatile long字段进行原子更新
3、AtomicReferenceFieldUpdater 基于反射的实用工具,可以对指定类的指定volatile Reference进行原子更新
4、AtomicInteger 可以用原子方式更新的 int 值。(使用场景,操作共享数据时,涉及多线程并发时,把变量设置成此类型,解决多线程并发问题)
(3)java.util.concurrent.locks 为锁定和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。
此包下面类
TimeUnit 表示给定单元粒度的时间段,它提供在这些单元中进行跨单元转换和执行计时及延迟操作的实用工具方法。

三、线程池的概念与Executors类应用(java.util.concurrent.Executors )
在线程池的编程模式下,任务是提交给整个线程池,而不是交给某个线程,线程池拿到任务后,它就在内部找到有无空闲的线程,再把任务交给内部某个空闲的线程,这就是封装。记住,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
ExecutorsService threadPool = Executors.newFixedThreadPool(3);
表示创建固定大小的线程池(例如一个包含3个线程的线程池)
ExecutorsService threadPool = Executors.newcachedThreadPool();
表示创建缓存线程池
ExecutorsService threadPool = Executors.newSingleThreadExecutor();
表示创建单一线程池
关闭线程池
shutdown与shutdownNow的比较
shutdown是当线程池中所有任务都完成了没有任务了,使用shutdown关闭线程池
shutdownNow表示还有任务没有干完也要关闭线程池

四、线程锁Lock
Lock lock= new ReentrantLock();
lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。二个线程执行的代码片段要实现同步互斥的效果,他们必须用同一把lock对象。锁是上在代表操作的资源的类的内部方法中,而不是线程代码中

condition(java.util.concurrent.locks包下的接口)
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
condition接口的方法
(1)(void)await()造成当前线程在接到信号或被中断之前一直处于等待状态。
(2)(boolean)await(long time, TimeUnit unit) 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
(3)(void)signal(): 唤醒一个等待线程
(4)(void)signalAll(): 唤醒所有等待线程

ReadWriterLock rwl=new ReentrantReadWriteLock();
读写锁(ReadWriteLock):分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,这是由jvm自己控制,你只要上好相应的锁即可。如果你的代码只读数据,可以有很多人同时读,但不能同时写,那么就上读锁,如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁,总之,读的时候上读锁,写的时候上写锁

读写锁ReadWriteLock和lock锁的区别,读写锁,当上了读锁后,还可以有多个线程来读数据,但不能修改数据,当上了写锁后,只有一个线程操作写数据,别的线程不能进入读和写,这点和lock比较像;而lock是当上锁后,只有我可以进入执行,所有的线程都不能进入。

五、阻塞队列
http://www.cnblogs.com/dolphin0520/p/3932906.html
阻塞队列 interface blockingQueue extends Queue
blockingQueue不接受null值,试图add,put,offer一个null元素,就会抛出空指针异常
blockingQueue是线程安全的,所以排队的方法都可以使用内部锁定或者其他形式的并发控制来自动达到它们的目的。
已知的接口实现类:ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue
(1)几种主要的阻塞队列
自从Java 1.5之后,在java.util.concurrent包下提供了若干个阻塞队列,主要有以下几个:
ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。
DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
(2)阻塞队列中的方法 VS 非阻塞队列中的方法
1.非阻塞队列中的几个主要方法:

  add(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常;

  remove():移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常;

  offer(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false;

  poll():移除并获取队首元素,若成功,则返回队首元素;否则返回null;

  peek():获取队首元素,若成功,则返回队首元素;否则返回null
对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。注意,非阻塞队列中的方法都没有进行同步措施。

2.阻塞队列中的几个主要方法:

  阻塞队列包括了非阻塞队列中的大部分方法,上面列举的5个方法在阻塞队列中都存在,但是要注意这5个方法在阻塞队列中都进行了同步措施。除此之外,阻塞队列提供了另外4个非常有用的方法:

  put(E e)

  take()

  offer(E e,long timeout, TimeUnit unit)

  poll(long timeout, TimeUnit unit)

  put方法用来向队尾存入元素,如果队列满,则等待;

  take方法用来从队首取元素,如果队列为空,则等待;

  offer方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;

  poll方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;

六 Java并发集合类

这里先提到一个概念:就是CAS(Compare and Swap, 翻译成比较并交换),Java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。Java并发库中的AtomicXXX类均是基于这个CAS操作的。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS还是没有搞明白,具体是什么东东(先放着吧,后面再说)

concurrentHashMap 代替同步的Map(Collections.synchronized(new HashMap())),众所周知,hashMap是根据散列值分段存储的,同步map的时候,会同步的时候锁住所有段的数据, 而concurrentHashMap加锁的时候是根据散列值锁住散列值锁对应的那段,所以提高了并发

HashMap不是线程安全的。Hashtable是线程安全的,但是由于Hashtable是采用synchronized进行同步,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。
HashTable容器使用synchronized来保证线程安全,在线程竞争激烈的情况下HashTable的效率非常低下,Collections.synchronizedXxx()同步容器等相比,而util.concurrent包的引入,就是解决并发:
解决2个问题:1.根据具体场景进行设计,尽量的避免使用synchronized,以提高并发;2.定义一些并发安全的复合操作,并且保证在并发的环境下迭代操作不会出错。

ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。

CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是”克隆”容器对象。

ConcurrentHashMap的内部结构
ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下ConcurrentHashMap的内部结构:
这里写图片描述 从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

Segment我们再来具体了解一下Segment的数据结构:

static final class Segment<K,V> extends ReentrantLock implements Serializable {    transient volatile int count;    transient int modCount;    transient int threshold;    transient volatile HashEntry<K,V>[] table;    final float loadFactor;}

详细解释一下Segment里面的成员变量的意义:

count:Segment中元素的数量
modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)
threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
table:链表数组,数组中的每一个元素代表了一个链表的头部
loadFactor:负载因子,用于确定threshold 

HashEntry (Segment中的元素是以HashEntry的形式存放在链表数组中的,看一下HashEntry的结构:)

static final class HashEntry<K,V> {    final K key;    final int hash;    volatile V value;    final HashEntry<K,V> next;}

可以看到HashEntry的一个特点,除了value以外,其他的几个变量都是final的,这样做是为了防止链表结构被破坏,出现ConcurrentModification的情况。

并发容器之CopyOnWriteArrayList
Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

信号量(Semaphore)

有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。

    一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()和release()获取和释放访问许可。   Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
0 0
原创粉丝点击