您现在的位置是:首页 > 文章详情

Java之多线程

日期:2019-07-15点击:340

线程是任务调度和执行的基本单位,可以看做是轻量级的进程,多线程是指在同一程序中有多个顺序流在执行,也就是一个进程中同时执行多个线程,两个或两个以上的线程对同一个变量的操作.如果两个线程修改同一个对象的状态,根据线程访问数据的次序,可能会产生错误的数据,也就常说的并发问题.

线程的基本概念与创建

在学线程之前,首先来来了解一下线程与进程的区别:
进程:

  • 是系统资源分配的基本单位
  • 每个进程都有独立的代码和数据空间,程序切换会有较大的开销
  • 在操作系统中可以同时运行多个程序
  • 在运行时会为每个进程分配不同的内存空间

线程:

  • 是任务调度和执行的基本单位
  • 可以看做是轻量级的进程,同一类的线程共享代码和数据空间,线程之间切换开销较小
  • 多个线程可以运行在同一进程中
  • 除了CPU,系统不会为线程分配内存,线程组之间只能共享数据

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止
创建的线程有四种方式:

  • 继承Thread
  • 通过实现Runnable接口
  • 通过实现Callable接口
  • 通过线程池创建

1.继承Thread类

class TestThread extends Thread{ private volatile int ticket = 10; @Override public void run() { synchronized (this){ while (ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票操作 ticket = "+ticket--); } } } }

public class Test{ public static void main(String[] args) throws Exception{ TestThread testThread = new TestThread(); testThread.run(); } }

运行结果:

image

2.实现Runnable接口
经典的生产者和消费者

class Product implements Runnable{//生产者 private Message message; Product(Message message) { this.message = message; } public void run() { for (int x = 0 ; x < 100 ; x++){ if (x % 2 == 0){ this.message.set("生产者","生产"); }else { this.message.set("消费者","消费"); } } } } ---- class Consumer implements Runnable{//消费者 private Message message; public Consumer(Message message) { this.message = message; } public void run() { for (int x = 0 ; x < 100 ; x++){ System.out.println(this.message.get()); } } } ---- class Message{ //消息中心 private String title; private String content; private boolean flag = true;//表示生产或者消费的形式 // flag = true 允许生产,不允许消费 flag= false 允许消费,不允许生产 synchronized void set(String title, String content){ if (!flag){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.title = title; this.content = content; this.flag = false; //生产完成 notify();//唤醒等待的线程 } synchronized String get(){ if (flag == true){ try { super.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } try { return this.title + " ------------ "+this.content; }finally { this.flag = true; // 可继续生产 notify();//唤醒等待线程 } } } ---- public class Producer { public static void main(String[] args) throws Exception{ Message message = new Message(); new Thread(new Product(message),"生产者").start();//启动生产者线程 new Thread(new Consumer(message),"消费者").start();//启动消费者线程 } }

运行结果

image

3.实现Callable接口

class TheThread implements Callable<String>{ private boolean flag = false; @Override public String call() throws Exception { synchronized (this){ if (this.flag==false){ this.flag = true; return Thread.currentThread().getName()+"抢答成功"; }else { return Thread.currentThread().getName()+"抢答失败"; } } } }

public class Competition { public static void main(String[] args) throws ExecutionException, InterruptedException { TheThread theThread = new TheThread(); FutureTask<String> futureTaskA = new FutureTask<>(theThread); FutureTask<String> futureTaskB = new FutureTask<>(theThread); FutureTask<String> futureTaskC = new FutureTask<>(theThread); new Thread(futureTaskA,"竞赛者A").start(); new Thread(futureTaskB,"竞赛者B").start(); new Thread(futureTaskC,"竞赛者C").start(); System.out.println(futureTaskA.get()); System.out.println(futureTaskB.get()); System.out.println(futureTaskC.get()); } }

运行结果

image

4.使用线程池

class ExecutorTest{ public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.submit(new Runnable() { @Override public void run() { for (int i = 0;i<10;i++){ System.out.println(i); } } }); } }

运行结果

image

以上案例都是处理过的线程,都是通过使用了synchronized锁保证了原子性,也就是线程安全的操作,线程的安全的实现方式,除了使用锁之外,也可以用CAS实现无锁安全操作.

CAS无锁实现线程安全操作

示例:

class CAS{ private AtomicInteger ticket = new AtomicInteger(10); public void run() { while (ticket.get()>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票操作 ticket = "+ticket.getAndDecrement()); } } }

----

public class CASTest{ public static void main(String[] args) throws Exception{ CAS cas = new CAS(); cas.run(); } }

程序执行结果

image

以上是使用了atomic包下线程安全的AtomicInteger,下面是atomic包下的线程安全类以及部分源码
image
Atomiclnteger本身是个整型,最重要的属性是value,而value使用了volatile关键字,value的作用就是保证可见性,即一个线程修改了共享变量时,其他线程读取的是修改后的新值.

getAndIncrement()方法实现了自增操作.核心实现是先获取当前值和目标值如果compareAndSet(current,next)成功返回该方法的目标值.getAndDecrement()方法则实现了自减操作.

AtomicInteger中的CAS操作就是compareAndSet(),作用是每次从内存中根据内存偏移量(valueOffset)取出数据,将取出的值跟expect比较,如果数据一致就把内存中的值改为update.这样就保证了原子操作.

AtomicInteger主要实现了整型的原子操作,防止并发的情况下出现异常结果,内部主要依靠JDK中的unsafe类操作内存中的数据实现.volatile修饰符保证了可见性,CAS操作保证了原子操作

volatile关键字

若某一属性使用volatile关键字,则表示不操作副本,直接操作原始变量.
volatile关键字的两层含义:
一旦一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰之后,那么久具体两层语义:

  • 保证了不同线程对这个变量进行操作的可见性,即一个线程修改了某个共享变量的值,这个新值对其他线程来说是立即可见的.
  • 禁止进行指令重排序
    禁止指令重排序有两层意思:
  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

volatile关键字需要结合锁来使用
示例:

class MyThread implements Runnable{ private volatile int ticket = 10; @Override public void run() { synchronized (this){ while (ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票操作 ticket = "+ticket--); } } } }

public class Volatile{ public static void main(String[] args) throws Exception{ MyThread myThread = new MyThread(); new Thread(myThread,"票贩子A").start(); new Thread(myThread,"票贩子B").start(); new Thread(myThread,"票贩子C").start(); } }

运行结果
image
本案例与上面的继承Thread类的案例是同一个.
在并发编程中会遇到三个问题:原子性、可见性、有序性

  • 可见性:
    volatile关键字在这里的作用是当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值.保证了可见性.普通的共享变量不能保证可见性,因为普通变量被修改之后,什么时候写入主存是不确定的,当其他线程去读的时候,内存中可能还是原来的旧值,因此无法保证可见性.通过synchronized和Lock也能够保证可见性,synchronized和lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中,因此可以保证其可见性.
  • 原子性:
    java的内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和lock来实现.由于synchronized和lock能够保证一个时刻只有一个线程执行该代码块,那么自然不存在原子性问题,从而保证了原子性.
  • 有序性:
    java的内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程的执行,却会影响到多线程并发执行的正确性.

可以通过volatile关键字来保证一定的"有序性",也可以通过synchronized和lock来保证有序性,synchronized和lock保证每一刻只有一个线程执行同步代码,相当于让线程顺序执行同步代码,自然就保证了有序性.
java内存模型具备一些先天的"有序性",即不需要通过任何手段就能够保证有序性,这个通常也称为happens-before原则(先行发生原则).如果两个操作的执行次序无法从happens-before原则推导出来,那么他们就不能保证它们的有序性,虚拟机可以随意的对它们进行重排序.

happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返
    回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
    这8条原则摘自《深入理解Java虚拟机》。

守护线程

public class Daemon { public static void main(String[] args) throws Exception{ Thread userThread = new Thread(() -> { for (int x = 0 ; x < 10 ; x++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在执行 x = " + x); } },"执行线程"); Thread daemonThread = new Thread(() -> { for (int x = 0 ; x < Integer.MAX_VALUE ; x++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在执行 x = " + x); } },"守护线程"); daemonThread.setDaemon(true);//将daemonThread设为守护线程 userThread.start(); daemonThread.start(); } }

运行结果
image
在Thread类提供了以下的守护线程的操作方法,可自行查看源码

  • 设置为守护线程:
public final void setDaemon(boolean on) { checkAccess(); if (isAlive()) { throw new IllegalThreadStateException(); } daemon = on; }

  • 判断是否为守护线程
public final boolean isDaemon() { return daemon; } 

停止线程

public class StopThread { public static boolean flag = true; public static void main(String[] args) throws Exception{ new Thread(() -> { long num = 0; while (flag){ try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在执行 num = " + num++); } },"执行线程").start(); Thread.sleep(200);//线程运行200毫秒 flag = false;//停止线程 } }

运行结果
image

多线程操作中使用的启动线程的方法是Thread类的start()方法,原本也提供有stop()方法来停止线程,但在1.2之后已经废除,现在停止线程的方法可使用上面案例所使用方式.

以上是线程的基本概念以及基础操作,若有不足或错误,欢迎指出.

原文链接:https://yq.aliyun.com/articles/709067
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章