java多线程笔记-基础一

说到多线程,一大波初级程序员的内心是慌乱的,平日里接触到的业务就是增删改查,并未直面过多线程编程的场景,容易造成一种并未接触过多线程编程的错觉,其实我们平时写的Java Web项目在很多地方都用到了多线程,只是它站在我们的代码之后,在幕后为我们做了很多操作,为了更加深入的理解和研究多线程幕后的工作原理,我们来首先了解下多线程的相关基础知识,以便为之后的探索打下基础。

为什么使用多线程

我们都知道CPU是通过时间片的轮循来执行多个任务,这样给用户的感受是CPU在同时执行多个任务,效率比较高。同样,多线程也可以给用户造成一种感受:同一时间执行多个不同的任务,实现最大限度的利用CPU。

多线程的应用场景

多线程的应用场景很多,像平时用到的数据库、Tomcat服务器和quartz任务调度等都用到了多线程,比如数据库连接使用了多线程,Tomcat内部是由多线程实现的,quartz定时任务调与多线程联合使用,这些多线程的使用有效的提升计算和处理效率,提高系统的吞吐量

多线程的基础

线程的内部结构

Java虚拟机在程序运行时把管理的内存分为几个区域,如下图所示,其中线程独占区是线程的内存结构,包括虚拟机栈,本地方法栈和程序计数器。程序在运行时,所有的变量都存储在主存当中,线程将数据从主存拷贝到自己本地缓存中,在线程的工作区内对变量副本进行操作,然后操作后的变量值写回到内存当中。


JVM运行时数据区.jpg
  • 程序计数器:记录当前线程锁执行到的字节码的行号;
  • 本地方法栈:为虚拟机用到的Native方法服务,存储运行Native方法时用到的数据;
  • 虚拟机栈:存储每个方法运行时需要的数据(以栈帧为存储单位);
  • 方法区:存储虚拟机加载类信息、常量、静态变量、即时编译器编译后的代码等数据;
  • 堆:存储对象实例;

线程的状态

线程状态图.jpg

线程共包括以下5种状态:

  1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    • 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
    • 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    • 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

多线程基础API

  • currentThread(): 返回代码段正在被哪个线程调用;
  • isAlive(): 返回当前线程是否处于活动状态;
  • sleep(): 让当前正在执行的线程(this.currentThread())暂停执行,休眠指定的时间毫秒,注意这里不让出CPU资源的;
  • getId(): 取得线程的唯一标识;
  • stop(): 暴力停止线程,这种方式是作废过期的做法,因为它可能会造成清理工作不能完成,还有可能导致对锁对象解锁而无法同步数据;
  • interrupt(): 停止线程,只是在当前线程打停止标记,不能真正的停止线程
  • interrupted(): 查看当前线程是否已经中断,执行后将中断状态标志置为false;
  • isInterrupted(): 测试Thread对象是否已经中断状态,但是不清楚状态标志,注意这里与上一个interrupted()方法的区别,本方法面向任何线程,而interrupted()是针对当前线程的;
  • suspend(): 暂停线程;
  • resume(): 恢复线程,与suspend()方法联合使用,但是如果使用不当容易造成公共的同步对象的独占,使其他线程无法访问公共对象;另外使用suspend()方法暂停线程,还有可能造成数据不同步的现象;
  • yield(): 放弃当前的CPU资源,让其他任务占用CPU的时间,但是自己又会去参与抢占CPU资源,有可能出现刚让出CPU,却又马上抢占到CPU;
  • setPriority(): 设置线程的优先级,但并不是每次都优先执行优先级较高的线程的run()方法;
  • setDaemon(): 常常用setDaemon(true)来设置线程为守护线程,当没有用户线程后,守护线程自动销毁,最常见的守护线程为垃圾回收线程;

线程间通信

线程之间需要进行交互,他们之间通信有以下几种方式:

  1. 等待-通知机制: wait()方法与notify()或notifyAll()联合使用,这三个方法都是属于Object的方法;所以所有类都可以继承这三方法;
  • wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法。
  • notify()方法会唤醒一个等待当前对象的锁的线程。而notifyAll()顾名思义;就是唤醒所有在等待中的方法;
  • wait()和notify()方法要求在调用时线程已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或synchronized块中。
    2.生产者-消费者模式:通过队列来开辟出线程共享的一段内存,生产者线程向共享内存中存放信息,消费者线程从共享内存中消费消息,

线程安全

线程安全的问题是多个线程同时读写共享资源而没有做任何的同步措施,导致出现脏数据或者不可预见的结果的问题。

并发访问

为了解决避免出现非线程安全的问题,对对象和变量的访问有以下几种方式:

  1. 使用synchronized实现同步:synchronized可以修饰方法或者静态块,其原理是获取对象的锁对象,持有锁对象的线程访问完并释放锁之后,其他线程才能进行访问,下面来说下synchronized的用法:
  • 修饰一个代码块:
    synchronized(this) { 同步代码块 }:锁住的是this所指代的对象,当有任意一个线程正在执行 这段同步块时,其它任何访问 this所代表的对象的线程 都会被 阻塞,直到这段 同步代码块执行完成,因为执行完成时 之前锁住this对象的线程会释放锁,释放之后 ,其它线程才能操作this对象;
    synchronized(new Object) { 同步代码块 } : 同上锁住的对象是 obj对象,这样当某线程正在执行这段同步代码块时,其它线程只是会被obj阻塞,但是仍然可能访问 T类的对象的其它方法;
    synchronized(T.class) { 同步代码块 } 锁住 T 的所有对象, 只要有任意一个线程在访问 T, 其它线程再访问时都会被阻塞住; 包括T的 所有实例对象的方法访问 和 访问T的静态成员或静态方法;
  • 修饰方法:
    修饰非静态方法时,锁住的是正在执行这个同步方法的对象;
    修饰静态方法时,锁住的是这个类的所有对象。
  1. 使用ReetrantLock实现同步:Lock代替同步方法或同步代码块,Condition替代同步监视器的功能,有读写锁,可以实现读写互斥,读读不互斥的效果,在效率上相对synchronized关键字有很大的提升,下面我们来具体说一下ReentrantLock相对synchronized所具有的优势:
  • 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间非常上的同步块很有帮助。而在等待由 synchronized 产生的互斥锁时,会一直阻塞,是不能被中断的。
  • 可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁时非公平锁,ReentrantLock 默认情况下也是非公平锁,但可以通过构造方法 ReentrantLock(ture)来要求使用公平锁。
  • 锁可以绑定多个条件:ReentrantLock 对象可以同时绑定多个 Condition 对象(名曰:条件变量或条件队列),而在 synchronized 中,锁对象的 wait()和 notify()或 notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无需这么做,只需要多次调用 newCondition()方法即可。而且我们还可以通过绑定 Condition 对象来判断当前线程通知的是哪些线程(即与 Condition 对象绑定在一起的其他线程)。

你可能感兴趣的:(java多线程笔记-基础一)