首页 > 代码库 > 【Java】多线程_学习笔记

【Java】多线程_学习笔记

多线程

1、进程

         进程:当一个程序进入内存运行时,它就成为了进程。进程具有独立性、动态性、并发性。

A、独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

B、动态性:进程与程序的区别在于,程序是一个静态的指令集合,而进程是一个正在运行的指令集合。进程有时间的概念,有自己的生命周期和各种不同的状态,而程序不具备这种概念。

C、并发性:多个进程可以在单个处理器上并发执行,相互之间不会有影响。

2、线程

线程:线程是轻量级进程,是进程的执行单元,是进程的组成部分。线程具有独立性、并发性、共享性、抢占性。

A、独立性:每个线程也是独立运行的。

B、并发性:多个线程可以并发执行。

C、共享性:线程可以拥有自己的堆栈、计数器、局部变量等,但不拥有系统资源,同一进程里的多个线程共享父进程的系统资源。

D、抢占性:线程的执行是抢占式的,当前线程任何时候都可能被挂起,以便其他线程运行,一个线程也能结束另一个线程。

说明:一个程序运行后至少有一个进程,一个进程里至少有一个线程。

3、继承Thread类创建线程

         创建线程方法:

         public class classname extends Thread

         {

         public void run()

         {

         //重写父类的run()方法的方法体

}

public static void main(String[] args)

{

         new classname().start();

}

}

步骤:

A、定义Thread类的子类,重写run()方法,该run()方法的方法体代表了线程需要完成的任务,所以run()方法称为线程执行体。

B、创建Thread子类的实例,即创建了线程的对象。

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

         说明:

A、Java使用Thread类代表线程,所有的线程的对象都必须是Thread类或其子类的实例。

B、main方法代表主线程。

C、Thread.currentThread():这是一个类方法,返回当前执行的线程对象。

D、getName():这是一个实例方法,返回调用该方法的线程的名字。

E、setName(String name):该方法可以为线程设置名字,线程名字默认为Thread-0、Thread-1……

F、使用继承Thread类的方式创建线程时,多个线程无法共享线程类的实例变量。

4、实现Runnable接口创建线程类

public class Classname implements Runnable

{

         public void run()

         {

         //业务代码

}

public static void main(String[] args)

{

         Classname cl = new Classname;

         new Thread(cl,“新线程”).start();

}

}

步骤:

A、定义Runnable接口的实现类,并重写该接口的run()方法,run()同样是该线程的线程执行体。

B、创建Runnable实现类的实例,并以此实例作为Thread的Target来创建Thread对象,该Thread对象才是真正的线程对象。

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

说明:

A、在实现接口的类的run()方法中要获取当前线程对象必须使用类名调用,即Thread.currentThread(),而继承父类的run()方法中,可以使用this调用。

B、实现Runnable接口的线程,可以共享线程类(实际是Target类)的实例变量,因为继承类创建两个线程时会创建两个继承类实例,不同实例的实例变量指向的内存区域不同,而实现接口类创建线程时,提供的只是自己的引用来辅助Thread创建对象,这个引用可以重复使用,所以不会创建多个接口类对象,所以接口类对象里实例自然是指向同一存储区域。

5、使用CallableFuture创建线程

public class Classname

{

         public static void main(String[] args)

         {

         FutureTask<Integer> task = new Future<Integer>

(

         (Callable<Integer>)() ->

         {

                  int j = 0;

                  //业务代码

                  return j;

         }

)

new Thread(task,”有返回值的线程”).start();

try

{

         System.out.println(“子线程的返回值:”+task.get());

}

catch(Exception ex)

{

         ex.printStackTrace();

}

}

}

步骤:

A、创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。因为Callable接口在Java8中是函数式接口,所以可以直接使用Lambda创建Callable对象。

B、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。

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

D、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

6、线程的生命周期

         说明:在线程的生命周期中,一共有5中状态,新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。

7、新建状态

         A、使用new关键字创建线程后即处于新建状态。

         B、由JVM虚拟机为其分配内存,初始化成员变量值。

         C、此时线程对象没有动态特性,也不会执行。

8、就绪状态

         A、当线程对象调用start()方法后,该线程就处于就绪状态。

         B、JVM为其创建方法调用栈和程序计数器。

         C、此时线程可以运行了,但没有运行,何时运行,取决于JVM线程调度器的调度。

9、运行状态

         A、处于就绪状态的线程,获得了CPU,开始执行run()方法,此时处于运行状态。

         B、一个CPU在任何时刻只能运行一个线程。

10、阻塞状态

         A、阻塞状态就是说运行状态被中断,系统资源被收回,其他线程启动。

         B、线程调用sleep()方法主动放弃所占用的处理器资源,进入阻塞状态。

         C、线程调用阻塞式IO方法,在该方法返回之前,该线程被阻塞。

         D、线程试图获得一个同步监视器,但该监视器被其他线程持有,进入阻塞状态。

         E、线程在等待某个通知(notify)。

F、程序调用线程的suspend()方法将该线程挂起,进入阻塞状态,该方法容易导致死锁,应尽量避免使用。

H、该线程阻塞后,其他线程获得执行机会,该线程会在合适的时候进入就绪状态,重新开始。

I、调用sleep()方法时间到了,线程接触阻塞进入就绪状态。

J、线程调用的阻塞式IO方法已经放回,线程解除阻塞进入就绪状态。

K、线程成功获得了试图取得的同步监视器,线程解除阻塞进入就绪状态。

M、线程等待某个通知时,其他线程发出了一个通知,线程解除阻塞进入就绪状态。

N、处于挂起状态的线程被调用了resume()恢复方法,线程解除阻塞进入就绪状态。

11、死亡状态

         A、run()或call()方法执行结束,线程死亡。

         B、线程抛出一个未捕获的Exception或Error,线程死亡。

         C、直接调用该线程的stop()方法结束该线程,线程死亡,但容易死锁,不建议使用。

         E、主线程结束不会对其他线程造成影响。

F、测试某个线程是否死亡,调用线程对象的isAlive()方法,处于就绪、运行、阻塞状态时,返回TRUE,处于新建、死亡状态时,返回FALSE。

G、不要对死亡的线程调用start()方法来试图启动它,死亡就是死亡。

H、对新建状态的线程两次调用start()方法也是错误的。

 

12、控制线程

(1)join线程

A、Thread类提供了一个join()方法,线程1调用线程2的join()方法,线程1被阻塞,线程2将执行,线程2执行完毕后,线程1继续执行。

B、join():等待被join的线程执行完成。

C、join(long millis):等待被执行join的线程的时间最长为millis毫秒,超过不等。

D、join(long millis,int nanos):等待被join的线程的时间最长millis毫秒+nanos微秒。

(2)后台线程

A、在后台运行的、为其他线程提供服务的线程,称为后台线程(Daemon Thread)、守护线程或精灵线程。

B、前台线程都死亡,后台线程会自动死亡。

C、调用Thread对象的setDaemon(true)方法,将指定线程设置称为后台线程。

D、Thread类提供isDaemon()方法,用于判断指定线程是否是后台线程。

(3)线程睡眠:sleep()

想让当前执行的线程暂停一段时间,进入阻塞状态,可以通过调用Thread类的静态方法sleep方法实现,sleep()有两种重载形式。

A、static void sleep(long millis):让当前执行的线程暂停millis毫秒,并进入阻塞状态,受系统计时器和线程调度器精度影响。

B、static void sleep(long millis,int nanos):让当前执行的线程暂停millis毫秒+nanos微秒,并进入阻塞状态,受系统计时器和线程调度器精度影响。

C、睡眠时间内,即使没有其他线程运行,该线程也不会的到运行,因此sleep()方法常用于暂停线程执行。

(4)线程让步:yield

A、yield()方法也是Thread类提供的类方法,也可以让当前线程暂停,但它不会阻塞当前线程,只是让它进入就绪状态,等待线程调度器的重新调用。

B、调用了yield()方法的线程,只有优先级与当前线程相同或者更高,才有获得执行的机会。

(5)yield()方法和sleep()方法的区别:

   sleep():不理会其他线程优先级。

    A、优先级

                                   yield():只给优先级相同或更高的执行。

                                   sleep():调用后进入阻塞状态,经过阻塞时间后转入就绪状态。

         B、阻塞

                                   yield():强制转入就绪状态。

                                   sleep():抛出InterruptedException异常,要么捕捉,要么显式声明抛出。

         C、异常

                                   yield():不抛出异常。

                                   sleep():较好。

         D、移植性

                  yield():较差,一般不建议使用yield()方法控制并发线程执行。

 (6)改变线程优先级

                  A、线程执行时都有一定的优先级,优先级高的线程获得的执行机会更多。

B、每个线程默认优先级和创建它的父线程相同,例如main线程是普通优先级,那么,它创建的子线程的优先级也是普通级。

C、Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,newPriority可以是1-10里的一个整数,也可是如下常量:

         a、MAX_PRIORITY:其值是10

         b、NORM_PRIORITY:其值是5

         c、MIN_PRIORITY:其值是1

D、尽量使用常量来设置优先级来保证可移植性。

13、线程同步

         (1)当两个进程并发修改同一个文件时就有可能造成异常。

         (2)多线程引入同步监视器解决这个问题,格式如下:

                  synchronized(obj)

                  {

         …

         //同步代码块

}

说明:obj就是同步监视器,上述代码的意思是,线程执行同步代码前,必须先获得同步监视器的锁定。

a、任何时刻,只有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。

b、任何对象都能作为同步监视器,但是推荐使用可能被并发访问的共享资源充当同步监视器。

c、使用同步监视器,可以保证并发线程同一时刻只有一个线程可以进入修改共享资源的代码区(也称为临界区),从而保证线程的安全性。

(3)同步方法:使用synchronized修饰的方法。对于synchronized修饰的实例方法(非static)而言,无须显式指定同步监视器,同步方法的监视器就是this,就是调用该方法的对象。

(4)不要对线程安全类的所以方法都进行同步,同步是以牺牲效率为代价的。

(5)释放同步监视器的锁定

任何线程进入同步代码块的时候都要先获得对同步监视器的锁定,但同步监视器的锁定的释放不是显性的,只有一下情况会释放锁定:

a、当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。

b、当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。

c、当前线程在同步代码块、同步方法中出现了未处理的Error或者Exception,导致了改代码块、该方法异常结束时,释放同步监视器。

d、当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,释放同步监视器。

以下情况,不会释放同步监视器:

a、线程执行同步代码块、同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程,不会释放同步监视器。

b、线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。

14、同步锁(Lock)

在实现线程安全的过程中,比较常用的是ReentrantLock(可重入锁),使用该对象可以显式的加锁、释放锁,格式如下:

class x

{

         private final ReentrantLock lock = new ReentrantLock();

         public void m()

         {

         lock.lock();

         try

         {

         //需要保证线程安全的代码

}

finally

{

         lock.unlock();

}

}

}

15、死锁

         死锁:当两个线程互相等待对方释放同步监视器时,就会进入死锁。

后果:整个程序既不会发生异常,也不会给出任何提示,所有线程处于阻塞状态,无法继续执行。

16、线程通信

         (1)传统线程通信:

a、wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或者notifyAll()方法来唤醒。

wait():一直等待,直至都到通知唤醒。

wait(long millis):等待millis毫秒后,自动苏醒。

wait(long millis,int nanos):等待millis毫秒+nanos微秒后,自动苏醒。

调用wait方法时,线程会释放该同步监视器的锁定。

b、notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,那么选择性的唤醒一个线程,选择是任意的。唤醒的线程要被执行的条件是,当前线程使用wait()方法等待,放弃对同步监视器的锁定。

c、notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对同步监视器的锁定后,唤醒的线程才有被执行的机会。

synchronized修饰的同步方法的默认实例时this,可以在同步方法中直接调用。

synchronized修饰的同步代码块,同步监视器是括号里的对象,必须使用该对象来调用方法。

(2)使用Condition控制线程通信

当使用Lock对象来保证同步,系统中不存在隐式的同步监视器,所以不能使用wait、notify等方法来进行线程通信,这时需要一个Condition对象来代替同步监视器的功能,Condition对象是被绑在Lock对象上的,调用Lock对象里的newCondition()方法即可创建Condition对象。

a、await():和wait()类似,导致当前线程等待,有很多变体,比如long awaitNanos(long nanosTimeout)等,可以完成更丰富的等待操作。

b、signal():唤醒在此Lock对象上等待的单个线程,所有线程都在改Lock对象上等待,选择唤醒其中一个线程,选择是任意的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。

c、signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。

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

                          a、BlockingQueue是一个线程同步的接口,是Queue的子接口。

b、BlockingQueue特征:当生产者线程试图想BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞。当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

c、程序的两个线程交替的往BlockingQueue中放入元素、取出元素,就可以很好的控制线程通信。

17、线程组和未处理的异常

         A、Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理。

         B、用户创建的线程都属于指定组,没有显示指定,则属于默认组。

         C、默认情况下,子线程和创建它的父线程在同一个组内。

         D、一旦某个线程加入指定组后,该线程一直属于该组,直至死亡。

E、ThreadGroup类提供两个构造器来创建实例:

a、ThreadGroup(String name):以指定的线程组名字来创建新的线程组。

b、ThreadGroup(ThreadGroup parent,String name):以指定的父线程和指定的名         字创建一个线程组。

c、可用getName()方法来获取线程组名字。

F、可用以下方法来操作整个线程组:

a、int activityCount():返回该线程组中活动的线程数目。

b、interrupt():中断此线程组中的所以线程。

c、isDaemon():判断该线程组是否是后台线程组。

d、setDaemon(boolean daemon):把该线程组设置成后台线程组。

e、setMaxPriority(int pri):设置线程组的最高优先级。    

G、void uncaughtException(Thread t,Throwable e)

ThreadGroup里定义的该方法可以处理该线程内任意线程所抛出的未处理异常,t 是出现异常的线程,e代表该线程抛出的异常。

        

 

【Java】多线程_学习笔记