java基础之线程(重点)

来源:互联网 发布:福建广播网络电视台 编辑:程序博客网 时间:2024/06/04 01:38

(一)线程与进程

线程本质:线程是一个程序里面不同的执行路径,即一个执行流程

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。

注意:进程是一个静态的概念,机器上的一个class文件或者exe文件就是一个进程,比如,程序的执行,首先将class文件加载到代码区,这时进程还没有开始执行,但进程已经产生了。平常说的进程的执行指的是主线程执行即main()方法的执行。

线程:线程是进程中的一个执行流程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。


(二)线程的创建与执行

1、线程有三种创建方法

第一种方法:继承java.lang.Thread类,子类必须重写run()方法,该方法作为新线程的入口点,同时,必须调用start()方法才能执行。(start()方法是通知cpu,线程我准备好了)

构造方法:publicThread(Runnable  target)

注意:Thread类实际上是实现了Runnable接口的类。

第二种方法:实现java.lang.Runnable接口,实现public  void   run()方法,该方法用于定义线程的运行体。(推荐使用)

注意:使用Runnable接口可以为多个线程提供共享的数据。

在run()方法中可以使用Thread类的静态方法:currentThread()方法:获取当前线程的引用

第三中方法:

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

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

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

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

public class CallableThreadTest implements Callable<Integer> {    public static void main(String[] args)      {          CallableThreadTest ctt = new CallableThreadTest();          FutureTask<Integer> ft = new FutureTask<>(ctt);          for(int i = 0;i < 100;i++)          {              System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);              if(i==20)              {                  new Thread(ft,"有返回值的线程").start();              }          }          try          {              System.out.println("子线程的返回值:"+ft.get());          } catch (InterruptedException e)          {              e.printStackTrace();          } catch (ExecutionException e)          {              e.printStackTrace();          }        }    @Override      public Integer call() throws Exception      {          int i = 0;          for(;i<100;i++)          {              System.out.println(Thread.currentThread().getName()+" "+i);          }          return i;      }  }


总结:
  • 1. 采用实现 Runnable、Callable 接口的方式创见多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

  • 2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。


(三)线程的状态

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

技术分享

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程控制方法:

isAlive()方法:判断线程是否还活着,即线程是否终止

getPriority()方法:获得线程的优先级数值。

setPriority()方法:设置线程的优先级数值。

注意:线程的优先级无法保证线程的执行次序,只不过,优先级高的线程获得CPU资源的概率比较大,获得CPU的执行时间越多。优先级低的也有机会执行。优先级用1-10之间的整数表示,默认是5(可省略),数值越大,优先级越高。

Thread.sleep():将当前线程睡眠指定毫秒数

join()方法:调用某线程的该方法,将当前线程和该线程合并,即等待该线程结束,再恢复当前线程的运行

yield()方法:让出CPU,当前线程进入就绪队列等待调度。

wait()方法:当前线程进入阻塞状态,让出CPU控制权,当前线程进入对象的wait pool。

notify()方法、notifyAll()方法:唤醒对象的wait  pool 中的一个或者所有等待的线程


(重点)sleep()和wait()的区别

1、每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;

3、sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;


(四)sleep / join / yield 方法

1、sleep(long   millis)方法:是Thread类的静态方法,睡眠指定的毫秒数。一秒=1000毫秒

注意:sleep(long  millis)方法是静态方法,在那个线程中调用该方法,就让那个线程睡眠。

interrupt()方法:中断线程,会抛出一个异常,进行处理(太粗暴,不推荐使用)

stop()方法:中断线程,更加粗暴,直接杀死线程。(已过时,尽量不要使用)(重点)

注意:中断是一个状态,interrupt()方法只是将这个状态置为true而已。它并不像stop()方法那样会中断一个正在运行的线程,线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。

Thread.interrupted()检查当前线程是否发生中断,返回boolean
synchronized在获锁的过程中是不能被中断的。

因此,正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体

控制线程停止的方法:

在线程类中定义一个Boolean类型的属性,对于while()循环,将其Boolean类型的属性设为false即可。

2、join()方法:优先执行完当前线程

join()方法:表示先执行完该线程(即调用join方法的线程),再执行其他线程。

3、yield()方法:暂停当前正在执行的线程对象,并执行其他线程


(五)线程同步:共享资源一定要设置好同步,防止其他未设置同步的方法操作共享资源,引发冲突,造成数据错误(重点)

JDK1.5之前,采用关键字synchronized和监视器机制来实现线程同步,JDK1.5之后,采用锁的机制实现线程同步。

(1) synchronized 修饰非静态方法,此时在这个方法体内的就有了 ”监视器“,它是所在类的对象.

                                                synchronized void func(){}

(2) synchronized 修饰静态方法,此时在这个方法体内的也有了 ”监视器“,它是所在类的对象.

                                                static synchronized void func(){}

(3) synchronized 修饰语句块,此时需要一个参数,而这个参数就作为“ 监视器 ”。

                                               synchronized(Object o){ }

监视器就是一个对象,不过他是有条件的,线程获取监视器才能执行synchronized修饰的方法或者代码。

java.util.concurrent.locks包提供了锁和等待条件的接口和类, 可用于替代JDK1.5之前的同步(synchronized)和监视器机制(主要是Object类的wait(), notify(), notifyAll()方法).

所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 

而现在, Lock提供了比synchronized机制更广泛的锁定操作。

Lock和synchronized机制的主要区别:

  • synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放. 
  • Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能. 以下代码演示了在不同的块结构中获取和释放锁。
1、线程同步:线程可能和其他线程共享一些资源,比如:文件,数据,内存等。当多个线程同时读写共享资源时,就可能发生冲突,为了解决这个问题,引入了线程同步机制,线程同步的真实意思,其实是“排队”:几个线程之间要排队,依次对共享资源进行操作,而不是同时进行操作。保证数据的准确性和唯一性,实现线程安全。

2、线程安全多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,必须等该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 

注意:对全局变量和静态变量操作在多线程模型中会引发线程安全问题。

3、并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。相当于两个人同时做事情。

4、并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。相当于两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。

注意:需要将事务和线程同步区分开,二者是不同层面的术语,应用场景不同。

多线程为了提高应用的执行效率而设置的,事务为保证一个操作的原子性而设置的

形象说明:多线程相当于多个人同时工作,在工作的过程中,大家可能都需要上厕所(厕所只有一个),这时就需要进行排队,每个人依次上厕所,第一个上厕所的人需要上锁,防止其他人进入,用完解锁,下一个人再用。这就是多线程和线程同步。

(六)使用synchronized实现线程同步

synchronized关键字的用法(是关键字,内置特性)(重点)

1、synchronized的用法(作用:同一时间只能一个线程执行synchronized修饰的代码或者方法)

synchronized代码块使用格式:synchronized(synObject){  //程序代码  }

注意:synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。

synchronized方法:就是将synchronized放在方法声明中,表示整个方法是同步方法。

例如:public   synchronized   void   add(String    str){  //方法体   }

注意:每个类也会有一个锁(类锁),它可以用来控制对static数据成员的并发访问。


2、Lock接口的用法ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

问题:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:

  如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

  因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

注意:Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

  1. public interface Lock {  
  2.     void lock();  
  3.     void lockInterruptibly() throws InterruptedException;  
  4.     boolean tryLock();  
  5.     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  
  6.     void unlock();  
  7.     Condition newCondition();  
  8. }  
其中lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。

<1>.lock():如果锁已被其他线程获取,则进行等待。采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。


<2>.tryLock():有返回值,表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit):  tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。


<3>.lockInterruptibly()

获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用thread B.interrupt()方法能够中断线程B的等待过程。

 注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

  因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

总结:

      1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

(七)死锁示例

类似案例:五个科学家吃放,互相占用筷子,最后饿死。

对于多线程,最基本的原则是:线程安全大于性能,一定要保证线程安全。

public class DealThread implements Runnable{public int flag = 1;static static Object o1 = new Object(),o2 = new Object();   //注意这里是静态的对象@Overridepublic void run() {System.out.println("flag="+flag);if (flag == 1) {synchronized (o1) {try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}synchronized (o2) {System.out.println("1");}}}if (flag == 0) {synchronized (o2) {try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}synchronized (o1) {System.out.println("0");}}}}public static void main(String[] args) {DealThread d1 = new DealThread();DealThread d2 = new DealThread();d1.flag = 1;d2.flag = 0;Thread t1 = new Thread(d1);Thread t2 = new Thread(d2);t1.start();t2.start();}}

参考文档:http://blog.csdn.net/g893465244/article/details/52538598


原创粉丝点击