Java 高并发二:多线程基础详细介绍

本系列基于炼数成金课程,为了更好的学习,做了系列的记录。 本文主要介绍 1.什么是线程 2.线程的基本操作 3.守护线程 4.线程优先级 5.基本的线程同步操作

1. 什么是线程

线程是进程内的执行单元

某个进程当中都有若干个线程。

线程是进程内的执行单元。

使用线程的原因是,进程的切换是非常重量级的操作,非常消耗资源。如果使用多进程,那么并发数相对来说不会很高。而线程是更细小的调度单元,更加轻量级,所以线程会较为广泛的用于并发设计。

在Java当中线程的概念和操作系统级别线程的概念是类似的。事实上,Jvm将会把Java中的线程映射到操作系统的线程区。

2. 线程的基本操作

2.1 线程状态图

上图是Java中线程的基本操作。

当new出一个线程时,其实线程并没有工作。它只是生成了一个实体,当你调用这个实例的start方法时,线程才真正地被启动。启动后到Runnable状态,Runnable表示该线程的资源等等已经被准备好,已经可以执行了,但是并不表示一定在执行状态,由于时间片轮转,该线程也可能此时并没有在执行。对于我们来说,该线程可以认为已经被执行了,但是是否真实执行,还得看物理cpu的调度。当线程任务执行结束后,线程就到了Terminated状态。

有时候在线程的执行当中,不可避免的会申请某些锁或某个对象的监视器,当无法获取时,这个线程会被阻塞住,会被挂起,到了Blocked状态。如果这个线程调用了wait方法,它就处于一个Waiting状态。进入Waiting状态的线程会等待其他线程给它notify,通知到之后由Waiting状态又切换到Runnable状态继续执行。当然等待状态有两种,一种是无限期等待,直到被notify。一直则是有限期等待,比如等待10秒还是没有被notify,则自动切换到Runnable状态。

2.2 新建线程

Thread thread = new Thread();
thread.start();

这样就开启了一个线程。
有一点需要注意的是

Thread thread = new Thread();
thread.run();

直接调用run方法是无法开启一个新线程的。

start方法其实是在一个新的操作系统线程上面去调用run方法。换句话说,直接调用run方法而不是调用start方法的话,它并不会开启新的线程,而是在调用run的当前的线程当中执行你的操作。

Thread thread = new Thread("t1")
{
 @Override
 public void run()
 {
 // TODO Auto-generated method stub
 System.out.println(Thread.currentThread().getName());
 }
};
thread.start();
如果调用start,则输出是t1
Thread thread = new Thread("t1")
{
 @Override
 public void run()
 {
 // TODO Auto-generated method stub
 System.out.println(Thread.currentThread().getName());
 }
};
thread.run();

如果是run,则输出main。(直接调用run其实就是一个普通的函数调用而已,并没有达到多线程的作用)

run方法的实现有两种方式

第一种方式,直接覆盖run方法,就如刚刚代码中所示,最方便的用一个匿名类就可以实现。

Thread thread = new Thread("t1")
{
 @Override
 public void run()
 {
 // TODO Auto-generated method stub
 System.out.println(Thread.currentThread().getName());
 }
};

第二种方式

Thread t1=new Thread(new CreateThread3());

CreateThread3()实现了Runnable接口。

在张孝祥的视频中,推荐第二种方式,称其更加面向对象。

2.3 终止线程

Thread.stop() 不推荐使用。它会释放所有monitor
在源码中已经明确说明stop方法被Deprecated,在Javadoc中也说明了原因。

原因在于stop方法太过"暴力"了,无论线程执行到哪里,它将会立即停止掉线程。

当写线程得到锁以后开始写入数据,写完id = 1,在准备将name = 1时被stop,释放锁。读线程获得锁进行读操作,读到的id为1,而name还是0,导致了数据不一致。

最重要的是这种错误不会抛出异常,将很难被发现。

2.4 线程中断

线程中断有3种方法

public void Thread.interrupt() // 中断线程
public boolean Thread.isInterrupted() // 判断是否被中断
public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态

什么是线程中断呢?

如果不了解Java的中断机制,这样的一种解释极容易造成误解,认为调用了线程的interrupt方法就一定会中断线程。
其实,Java的中断是一种协作机制。也就是说调用线程对象的interrupt方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时机中断自己。每个线程都有一个boolean的中断状态(不一定就是对象的属性,事实上,该状态也确实不是Thread的字段),interrupt方法仅仅只是将该状态置为true。对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true,并不会使程序停止;

public void run(){//线程t1
 while(true){
 Thread.yield();
 }
}
t1.interrupt();

这样使线程t1中断,是不会有效果的,只是更改了中断状态位。

如果希望非常优雅地终止这个线程,就该这样做

public void run(){
 while(true)
 {
 if(Thread.currentThread().isInterrupted())
 {
  System.out.println("Interruted!");
  break;
 }
 Thread.yield();
 }
}

使用中断,就对数据一致性有了一定的保证。

对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为false.

对于取消阻塞状态中的线程,可以这样抒写代码:

public void run(){
 while(true){
 if(Thread.currentThread().isInterrupted()){
  System.out.println("Interruted!");
  break;
 }
 try {
  Thread.sleep(2000);
 } catch (InterruptedException e) {
  System.out.println("Interruted When Sleep");
  //设置中断状态,抛出异常后会清除中断标记位
  Thread.currentThread().interrupt();
 }
 Thread.yield();
 }
}

2.5 线程挂起

挂起(suspend)和继续执行(resume)线程

suspend()不会释放锁

如果加锁发生在resume()之前 ,则死锁发生
这两个方法都是Deprecated方法,不推荐使用。

原因在于,suspend不释放锁,因此没有线程可以访问被它锁住的临界区资源,直到被其他线程resume。因为无法控制线程运行的先后顺序,如果其他线程的resume方法先被运行,那则后运行的suspend,将一直占有这把锁,造成死锁发生。

用以下代码来模拟这个场景

package test;

public class Test
{
 static Object u = new Object();
 static TestSuspendThread t1 = new TestSuspendThread("t1");
 static TestSuspendThread t2 = new TestSuspendThread("t2");

 public static class TestSuspendThread extends Thread
 {
 public TestSuspendThread(String name)
 {
 setName(name);
 }

 @Override
 public void run()
 {
 synchronized (u)
 {
 System.out.println("in " + getName());
 Thread.currentThread().suspend();
 }
 }
 }

 public static void main(String[] args) throws InterruptedException
 {
 t1.start();
 Thread.sleep(100);
 t2.start();
 t1.resume();
 t2.resume();
 t1.join();
 t2.join();
 }
}

让t1,t2同时争夺一把锁,争夺到的线程suspend,然后再resume,按理来说,应该某个线程争夺后被resume释放了锁,然后另一个线程争夺掉锁,再被resume。
结果输出是:

in t1
in t2

说明两个线程都争夺到了锁,但是控制台的红灯还是亮着的,说明t1,t2一定有线程没有执行完。我们dump出堆来看看

发现t2一直被suspend。这样就造成了死锁。

2.6 join和yeild

yeild是个native静态方法,这个方法是想把自己占有的cpu时间释放掉,然后和其他线程一起竞争(注意yeild的线程还是有可能争夺到cpu,注意与sleep区别)。在javadoc中也说明了,yeild是个基本不会用到的方法,一般在debug和test中使用。

join方法的意思是等待其他线程结束,就如suspend那节的代码,想让主线程等待t1,t2结束以后再结束。没有结束的话,主线程就一直阻塞在那里。

package test;

public class Test
{
 public volatile static int i = 0;

 public static class AddThread extends Thread
 {
 @Override
 public void run()
 {
 for (i = 0; i < 10000000; i++)
 ;
 }
 }

 public static void main(String[] args) throws InterruptedException
 {
 AddThread at = new AddThread();
 at.start();
 at.join();
 System.out.println(i);
 }
}

如果把上述代码的at.join去掉,则主线程会直接运行结束,i的值会很小。如果有join,打印出的i的值一定是10000000。
那么join是怎么实现的呢?

join的本质

while(isAlive())
{
   wait(0);
}

join()方法也可以传递一个时间,意为有限期地等待,超过了这个时间就自动唤醒。
这样就有一个问题,谁来notify这个线程呢,在thread类中没有地方调用了notify?

在javadoc中,找到了相关解释。当一个线程运行完成终止后,将会调用notifyAll方法去唤醒等待在当前线程实例上的所有线程,这个操作是jvm自己完成的。

所以javadoc中还给了我们一个建议,不要使用wait和notify/notifyall在线程实例上。因为jvm会自己调用,有可能与你调用期望的结果不同。

3. 守护线程

在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。
当一个Java应用内,所有非守护进程都结束时,Java虚拟机就会自然退出。
此前有写过一篇python中如何实现,查看这里。

而Java中变成守护进程就相对简单了。

Thread t=new DaemonT();
t.setDaemon(true);
t.start();

这样就开启了一个守护线程。

package test;

public class Test
{
 public static class DaemonThread extends Thread
 {
 @Override
 public void run()
 {
 for (int i = 0; i < 10000000; i++)
 {
 System.out.println("hi");
 }
 }
 }

 public static void main(String[] args) throws InterruptedException
 {
 DaemonThread dt = new DaemonThread();
 dt.start();
 }
}

当线程dt不是一个守护线程时,在运行后,我们能看到控制台输出hi
当在start之前加入

dt.setDaemon(true);

控制台就直接退出了,并没有输出。

4. 线程优先级

Thread类中有3个变量定义了线程优先级。

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
package test;

public class Test
{
 public static class High extends Thread
 {
 static int count = 0;
 @Override
 public void run()
 {
 while (true)
 {
 synchronized (Test.class)
 {
 count++;
 if (count > 10000000)
 {
 System.out.println("High");
 break;
 }
 }
 }
 }
 }
 public static class Low extends Thread
 {
 static int count = 0;
 @Override
 public void run()
 {
 while (true)
 {
 synchronized (Test.class)
 {
 count++;
 if (count > 10000000)
 {
 System.out.println("Low");
 break;
 }
 }
 }
 }
 }

 public static void main(String[] args) throws InterruptedException
 {
 High high = new High();
 Low low = new Low();
 high.setPriority(Thread.MAX_PRIORITY);
 low.setPriority(Thread.MIN_PRIORITY);
 low.start();
 high.start();
 }
}

让一个高优先级的线程和低优先级的线程同时争夺一个锁,看看哪个最先完成。
当然并不一定是高优先级一定先完成。再多次运行后发现,高优先级完成的概率比较大,但是低优先级还是有可能先完成的。

5. 基本的线程同步操作

synchronized 和 Object.wait() Obejct.notify()

这一节内容详情请看以前写的一篇Blog

主要要注意的是

synchronized有三种加锁方式:

指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

作用于实例方法,则不要new两个不同的实例

作用于静态方法,只要类一样就可以了,因为加的锁是类.class,可以new两个不同实例。

wait和notify的用法:

用什么锁住,就用什么调用wait和notify

本文就不细说了。

(0)

相关推荐

  • Java 高并发七:并发设计模型详解

    1. 什么是设计模式 在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题 ,所提出的解决方案.这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领 域引入到计算机科学的. 著名的4人帮: Erich Gamma,Richard Helm, Ralph Johnson ,John Vlissides (Gof) <设计模式:可复用面向对象软件的基础>收录23种模式 2. 单例模式 单例对象的类必须保证只有一个实例存在.许

  • java web如何解决瞬间高并发

    1.任何的高并发,请求总是会有一个顺序的 2.java的队列的数据结构是先进先出的取值顺序 3.BlockingQueue类(线程安全)(使用方法可以百度) 一般使用LinkedBlockingQueue 利用以上几点,我们可以把高并发时候的请求放入一个队列,队列的大小可以自己定义,比如队列容量为1000个数据,那么可以利用过滤器或者拦截器把当前的请求放入队列,如果队列的容量满了,其余的请求可以丢掉或者作出相应回复 具体实施: 利用生产者.消费者模型: 将队列的请求一一处理完. 上代码: /**

  • Java 高并发六:JDK并发包2详解

    1. 线程池的基本使用 1.1.为什么需要线程池 平时的业务中,如果要使用多线程,那么我们会在业务开始前创建线程,业务结束后,销毁线程.但是对于业务来说,线程的创建和销毁是与业务本身无关的,只关心线程所执行的任务.因此希望把尽可能多的cpu用在执行任务上面,而不是用在与业务无关的线程创建和销毁上面.而线程池则解决了这个问题,线程池的作用就是将线程进行复用. 1.2.JDK为我们提供了哪些支持 JDK中的相关类图如上图所示. 其中要提到的几个特别的类. Callable类和Runable类相似,但

  • Java 高并发一:前言

    1.关于高并发的几个重要概念 1.1 同步和异步 首先这里说的同步和异步是指函数/方法调用方面. 很明显,同步调用会等待方法的返回,异步调用会瞬间返回,但是异步调用瞬间返回并不代表你的任务就完成了,他会在后台起个线程继续进行任务. 1.2 并发和并行 并发和并行在外在表象来说,是差不多的.由图所示,并行则是两个任务同时进行,而并发呢,则是一会做一个任务一会又切换做另一个任务.所以单个cpu是不能做并行的,只能是并发. 1.3 临界区 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用

  • Java 高并发九:锁的优化和注意事项详解

    摘要 本系列基于炼数成金课程,为了更好的学习,做了系列的记录. 本文主要介绍: 1. 锁优化的思路和方法 2. 虚拟机内的锁优化 3. 一个错误使用锁的案例 4. ThreadLocal及其源码分析 1. 锁优化的思路和方法 在[高并发Java 一] 前言中有提到并发的级别. 一旦用到锁,就说明这是阻塞式的,所以在并发度上一般来说都会比无锁的情况低一点. 这里提到的锁优化,是指在阻塞式的情况下,如何让性能不要变得太差.但是再怎么优化,一般来说性能都会比无锁的情况差一点. 这里要注意的是,在[高并

  • Java 高并发八:NIO和AIO详解

    IO感觉上和多线程并没有多大关系,但是NIO改变了线程在应用层面使用的方式,也解决了一些实际的困难.而AIO是异步IO和前面的系列也有点关系.在此,为了学习和记录,也写一篇文章来介绍NIO和AIO. 1. 什么是NIO NIO是New I/O的简称,与旧式的基于流的I/O方法相对,从名字看,它表示新的一套Java I/O标 准.它是在Java 1.4中被纳入到JDK中的,并具有以下特性: NIO是基于块(Block)的,它以块为基本单位处理数据 (硬盘上存储的单位也是按Block来存储,这样性能

  • Java 高并发五:JDK并发包1详细介绍

    在[高并发Java 二] 多线程基础中,我们已经初步提到了基本的线程同步操作.这次要提到的是在并发包中的同步控制工具. 1. 各种同步控制工具的使用 1.1 ReentrantLock ReentrantLock感觉上是synchronized的增强版,synchronized的特点是使用简单,一切交给JVM去处理,但是功能上是比较薄弱的.在JDK1.5之前,ReentrantLock的性能要好于synchronized,由于对JVM进行了优化,现在的JDK版本中,两者性能是不相上下的.如果是简

  • Java 高并发三:Java内存模型和线程安全详解

    网上很多资料在描述Java内存模型的时候,都会介绍有一个主存,然后每个工作线程有自己的工作内存.数据在主存中会有一份,在工作内存中也有一份.工作内存和主存之间会有各种原子操作去进行同步. 下图来源于这篇Blog 但是由于Java版本的不断演变,内存模型也进行了改变.本文只讲述Java内存模型的一些特性,无论是新的内存模型还是旧的内存模型,在明白了这些特性以后,看起来也会更加清晰. 1. 原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰

  • Java 高并发十: JDK8对并发的新支持详解

    1. LongAdder 和AtomicLong类似的使用方式,但是性能比AtomicLong更好. LongAdder与AtomicLong都是使用了原子操作来提高性能.但是LongAdder在AtomicLong的基础上进行了热点分离,热点分离类似于有锁操作中的减小锁粒度,将一个锁分离成若干个锁来提高性能.在无锁中,也可以用类似的方式来增加CAS的成功率,从而提高性能. LongAdder原理图: AtomicLong的实现方式是内部有个value 变量,当多线程并发自增,自减时,均通过CA

  • Java 高并发四:无锁详细介绍

    在[高并发Java 一] 前言中已经提到了无锁的概念,由于在jdk源码中有大量的无锁应用,所以在这里介绍下无锁. 1 无锁类的原理详解 1.1 CAS CAS算法的过程是这样:它包含3个参数CAS(V,E,N).V表示要更新的变量,E表示预期值,N表示新值.仅当V 值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么 都不做.最后,CAS返回当前V的真实值.CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成 操作.当多个线程同时使用CAS操

随机推荐