Java多线程学习笔记

进程是指运行中的应用程序,每个进程都有自己独立的地址空间;

线程是进程中执行运算的最小单位,一个进程中可以有多个线程。

1、 创建线程的方法

(1)继承Thread类创建线程类

通过继承Thread类创建线程类的具体步骤和具体代码如下:

   • 定义一个继承Thread类的子类,并重写该类的run()方法;

   • 创建Thread子类的实例,即创建了线程对象;

   • 调用该线程对象的start()方法启动线程。


Java多线程学习笔记_第1张图片

(2)实现Runnable接口创建线程类

通过实现Runnable接口创建线程类的具体步骤和具体代码如下:

   • 定义Runnable接口的实现类,并重写该接口的run()方法;

   • 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。


Java多线程学习笔记_第2张图片

(3)通过Callable和Future创建线程

通过Callable和Future创建线程的具体步骤和具体代码如下:

• 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

• 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

• 使用FutureTask对象作为Thread对象的target创建并启动新线程。

• 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义如下:  


Java多线程学习笔记_第3张图片

继承Thread类和实现Runnable接口、实现Callable接口的区别。

继承Thread:线程代码存放在Thread子类run方法中。

  优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。

  劣势:已经继承了Thread类,无法再继承其他类。

实现Runnable:线程代码存放在接口的子类的run方法中。

  优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

  劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。

实现Callable:

  优势:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

  劣势:比较复杂、访问线程必须使用Thread.currentThread()方法

建议使用实现接口的方式创建多线程。


2、 线程的生命周期

2.1、 新建状态

从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;

2.2、 就绪状态

线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;

2.3、 运行状态

就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。

2.4、 阻塞状态

在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。

2.5、 死亡状态

run()方法完成后或发生其他终止条件时就会切换到终止状态。


3、 线程管理

3.1、 线程睡眠——sleep

让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep静态方法。

扩展:

因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

3.2、 线程让步——yield

yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,但是yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次

关于sleep()方法和yield()方的区别如下:

①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

3.3、 线程合并——join

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。

Thread类提供了join方法来完成这个功能,注意,它不是静态方法。

它有3个重载的方法:

void join():当前线程等该加入该线程后面,等待该线程终止。

void join(long millis):当前线程等待该线程终止的时间最长为millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度

void join(long millis,int nanos):等待该线程终止的时间最长为millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度

3.4、 设置线程的优先级

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会

Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,范围是1~10之间,也可以使用Thread类提供的三个静态常量:

Java多线程学习笔记_第4张图片

3.5、 正确结束线程

Thread.stop()等方法已经被废弃了,可以使用下面的方法:

    • 正常执行完run方法,然后结束掉;

    • 控制循环条件和判断条件的标识符来结束掉线程。

Java多线程学习笔记_第5张图片

4、 线程同步与锁。

    java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

1、同步方法      

    synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

代码如下:

public synchronized void save(){}

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2、同步代码块     

    synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

代码如下:

Java多线程学习笔记_第6张图片

    追加问题:如果同步函数被静态修饰之后,使用的锁是什么?静态方法中不能定义this!

  静态内存是:内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class  该对象类型是Class。

  所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。类名.class。代码如下:

Java多线程学习笔记_第7张图片

3、使用特殊域变量(volatile)实现线程同步

• volatile关键字为域变量的访问提供了一种免锁机制;

• 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;

• 因此每次使用该域就要重新计算,而不是使用寄存器中的值;

• volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

4、使用重入锁(Lock)实现线程同步

    ReentrantLock类是可重入、互斥、实现了Lock接口的锁

5、 线程通信

1、借助于Object类的wait()、notify()和notifyAll()实现通信

    线程执行wait()后,就放弃了运行资格,处于冻结状态;

    notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程

    notifyall(), 唤醒线程池中所有线程。

    注:(1) wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;

           (2) wait(),notify(),notifyall(),在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。

2、使用Condition控制线程通信

(1)将同步synchronized替换为显式的Lock操作;

(2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;

(3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。

3、使用阻塞队列(BlockingQueue)控制线程通信

BlockingQueue是一个接口,也是Queue的子接口。

BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

BlockingQueue提供如下两个支持阻塞的方法:

 (1)put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。

  (2)take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

6、 线程池

    合理利用线程池能够带来三个好处:

    1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

    2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

    3、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

6.1、使用Executors工厂类产生线程池

    Executor线程池框架的最大优点是把任务的提交和执行解耦。注意:这个类是一个接口。

    Executor仅只是一个执行线程的工具。

    它利用AS的类继承关系发现,Executor有一个ExecutorService 子接口。

    ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中。

    ExecutorService接口的默认实现类为ThreadPoolExecutor。

ThreadPoolExecutor的构造方法有以下:

    1:int corePoolSize(core:核心的) = > 线程池核心线程数最大值

    2:int maximumPoolSize  = > 线程池中线程的最大值

    3:long keepAliveTime  = > 非核心线程闲置超时时长

    4:TimeUnit unit  = > (时间单位)

    5:BlockingQueue workQueue = > 线程池所使用的缓冲队列

    6:ThreadFactory threadFactory = >创建线程使用的工厂

    7:RejectedExecutionHandler handler = > 这个主要是用来抛异常的

(1) 使用Executors的静态工厂类创建线程池的方法如下:

    1、newFixedThreadPool()

    作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变。

    2、newCachedThreadPool()

    作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。

    3、newSingleThreadExecutor()

    作用:该方法返回一个只有一个线程的线程池

    4、newScheduledThreadPool()

    作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。

    5、newSingleThreadScheduledExecutor()

    作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。


7、 死锁

    产生死锁的四个必要条件如下,即任意一个条件不满足既不会产生死锁。

(1)四个必要条件

互斥条件:资源不能被共享,只能被同一个进程使用

请求与保持条件:已经得到资源的进程可以申请新的资源

非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺

循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源

举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

(2)处理死锁的方法

忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;

检测死锁并恢复;

资源进行动态分配;

破除上面的四种死锁条件之一。

8、 补充

Hashtable与Hashmap的区别

hashtable与hashmap都实现了map接口

     (1):在hashtable中,无论是key还是value都不能为null

     (2):在hashmap中,null可以作为主键,这样的键只能有一个,但可以有一个或者多个键所对应的值为null,在使用get()方法返回的时 候,既可以表示hashmap中没有该键亦可以表示该键的值为null,索引在hashmap中判断某个键是否存在的时候,不能使用get()方法,应该 使用containsKey()方法。

    hashmap与hashtable最大的不同是hashmap是线程不安全的,在多线程环境下,需要手动实现同步机制,hashtable是线程安全的,方法是同步的,可直接用于多线程环境中。

什么是线程安全的

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果都是一样的,而且其他变量的值也和预期的是一样的,就是线程安全的。

java中的wait和notifyall有什么区别?

    (1):如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁

    (2):当有线程调用了对象的notifyall(唤醒所有wait线程)或者notify(随机唤醒一个线程),被唤醒的线程就会进入到该对象的锁池中去,锁池中的线程会去竞争该对象锁。notify可能会产生死锁

同步和异步有何不同,在什么情况下分别使用它们?

    (1):如果数据将在线程间共享,必须使用同步存取

    (2):如果应用程序在对象上调用了一个需要花费长时间执行的方法并且不需要让程序等待方法返回的时候,就应该使用异步编程。

同步交互---指发送一个请求,需要等待返回才能发送下一个请求

异步交互---指发送一个请求,不需要等待返回随时可以发送下一个请求

什么是线程池?

    线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程(提高线程复用,减少性能开销)。

为什么要用线程池

1. 线程池改进了一个应用程序的响应时间。

2. 线程池节省开销并可以在任务完成后回收资源。

3. 线程池根据当前在系统中运行的进程来优化线程时间片。

4. 线程池允许我们开启多个任务而不用为每个线程设置属性。

5. 线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。

6. 线程池可以用来解决处理一个特定请求最大线程数量限制问题。

总结:

        使用线程池主要就是为了减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;节约应用内存(线程开的越多,消耗的内存也就越大,最后死机)。

队列Queue

队列是先进先出。相对的,栈是后进先出。

阻塞队列提供的常用方法:

Java多线程学习笔记_第8张图片

add(e) remove() element() 方法不会阻塞线程。当不满足约束条件时,会抛出IllegalStateException 异常。例如:当队列被元素填满后,再调用add(e),则会抛出异常。

ffer(e) poll() peek() 方法即不会阻塞线程,也不会抛出异常。例如:当队列被元素填满后,再调用offer(e),则不会插入元素,函数返回false。

要想要实现阻塞功能,需要调用put(e) take() 方法。当不满足约束条件时,会阻塞线程。

阻塞队列

Java多线程学习笔记_第9张图片

ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,按照先进先出(FIFO)的原则对元素进行排序。

LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。按照先进先出的顺序进行排序。

PriorityBlockingQueue:一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。

DelayQueue:一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。

SynchronousQueue:一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,相当于其它队列,但它队列多了transfer和tryTransfer方法。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。

原文链接:https://www.cnblogs.com/snow-flower/p/6114765.html

你可能感兴趣的:(Java多线程学习笔记)