关于多线程:多线程并行并发线程安全一文解

32次阅读

共计 11778 个字符,预计需要花费 30 分钟才能阅读完成。

目录

1、什么是线程、多线程、并行、并发?2、为什么应用多线程?3、怎么创立线程?4、怎么保障线程平安?5、线程如何调度的?6、线程分类?7、其它

一、什么是线程、多线程?

首先咱们先理解下,程序、过程:程序:是一组计算机能辨认和执行的指令,运行于电子计算机上,满足人们某种需要的信息化工具。过程:正在运行的一个应用程序,是一个动静的过程,有它本身的产生,存在和沦亡的过程。(领有独立的内存空间)(例如:运行一个 word.exe 软件,就是一个过程)

线程:过程可进一步细化为线程,是一个程序外部的一条执行办法,有本人独立的工作内存 (栈) 和共享内存(堆)。(从宏观角度上了解线程是并行运行的,然而从宏观角度上剖析却是串行运行的,即一个线程一个线程的去运行,当零碎只有一个 CPU 时,线程会以某种程序执行多个线程,咱们把这种状况称之为线程调度。工夫片即 CPU 调配给各个程序的运行工夫(很小的概念))多线程:指的是这个过程运行时产生了不止一个线程。并行:指两个或多个事件在同一时刻点产生。(计算机系统中有多个 CPU,则这些能够并发执行的程序便可被调配到多个处理器上,实现多任务并行执行,即利用每个处理器来解决一个可并发执行的程序,这样,多个程序便能够同时执行,因为是宏观的,所以大家在应用电脑的时候感觉就是多个程序是同时执行的。)

并发:指两个或多个事件在同一时间段内产生。(在单 CPU 零碎中,每一时刻却仅能有一道程序执行(工夫片),故宏观上这些程序只能是分时地交替执行。)

二、为什么要应用多线程?

用线程只有一个目标,那就是更好的利用 cpu 的资源,因为所有的多线程代码都能够用单线程来实现。

三、怎么创立线程?

1、继承 Thread 类

步骤:
1): 定义一个类 A 继承于 java.lang.Thread 类.
2): 在 A 类中笼罩 Thread 类中的 run 办法.
3): 咱们在 run 办法中编写须要执行的操作 ---->run 办法里的, 线程执行体.
4): 在 main 办法 (线程) 中, 创立线程对象, 并启动线程.
留神:启动线程,必须用 start()办法,这样才能够创立线程,不可间接调用 run()办法,这样无奈创立线程,只是你在 main 办法调用了 run()办法,都只是 main 线程执行,没有创立新线程。

生产者与消费者例子(继承 Thread 版):

    package com.example.gxw.Thread;
    
    import android.view.Window;
    
    /**
     *
     * 创立三个窗口卖票,总票数为 100 张,应用继承自 Thread 形式
     * 用动态变量保障三个线程的数据独一份
     * 
     * 存在线程的平安问题,有待解决
     *
     * */
    
    public class ThreadDemo extends Thread{public static void main(String[] args){window t1 = new window();
            window t2 = new window();
            window t3 = new window();
    
            t1.setName("售票口 1");
            t2.setName("售票口 2");
            t3.setName("售票口 3");
    
            t1.start();
            t2.start();
            t3.start();}
    
    }
    
    class window extends Thread{
        // 将其加载在类的动态区,所有线程共享该动态变量
        private static int ticket = 100; 
    
        @Override
        public void run() {while(true){if(ticket>0){System.out.println(getName()+"以后售出第"+ticket+"张票");
                    ticket--;
                }else{break;}
            }
        }
    }

2、实现 Runnable 接口

步骤:
1): 定义一个类 A 实现于 java.lang.Runnable 接口, 留神 A 类不是线程类.
2): 在 A 类中笼罩 Runnable 接口中的 run 办法.
3): 咱们在 run 办法中编写须要执行的操作(run 办法里的, 线程执行体).
4): 在 main 办法 (线程) 中, 创立线程对象,并将 A 实现类做参传给线程结构器, 而后通过 start()办法启动线程.

生产者与消费者例子(实现 Runnable 版):

    package com.example.gxw.Thread;
    
    public class ThreadDemo01 {public static  void main(String[] args){window1 w = new window1();
            
            // 尽管有三个线程,然而只有一个窗口类实现的 Runnable 办法,因为三个线程共用一个 window 对象,所以主动共用 100 张票
            
            Thread t1=new Thread(w);
            Thread t2=new Thread(w);
            Thread t3=new Thread(w);
    
            t1.setName("窗口 1");
            t2.setName("窗口 2");
            t3.setName("窗口 3");
            
            t1.start();
            t2.start();
            t3.start();}
    }
    
    class window1 implements Runnable{
        
        private int ticket = 100;
    
        @Override
        public void run() {while(true){if(ticket>0){System.out.println(Thread.currentThread().getName()+"以后售出第"+ticket+"张票");
                    ticket--;
                }else{break;}
            }
        }
    }

3、实现 Callable 接口

步骤:
1): 定义一个类 A 实现于 java.util.concurrent.Callable 接口(JDK5 新增).
2): 在 A 类中笼罩 Callable 接口中的 call 办法.
3): 咱们在 call 办法中编写须要执行的操作.
4): 在 main 办法 (线程) 中:1、创立实现 Callable 接口的实现类
    2、创立 FutureTask 的对象
    3、将 callable 接口实现类的对象作为传递到 FutureTask 的结构器中
    4、创立 Thread 对象,将 FutureTask 的对象作为参数传递到 Thread 类的结构器中
    5、通过 Thread 对象的 start()办法启动线程.
    6、通过 FutureTask 的对象调用办法 get 能够获取线程中的 call 的返回值。

学习例子:

package com.example.gxw.Thread;


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 创立线程的形式三:实现 callable 接口。---JDK 5.0 新增
 * 是否多线程?否,就一个线程
 *
 * 比 runable 多一个 FutureTask 类,用来接管 call 办法的返回值。* 实用于须要从线程中接管返回值的模式
 * 
 * //callable 实现新建线程的步骤:* 1. 创立一个实现 callable 的实现类
 * 2. 实现 call 办法,将此线程须要执行的操作申明在 call()中
 * 3. 创立 callable 实现类的对象
 * 4. 将 callable 接口实现类的对象作为传递到 FutureTask 的结构器中,创立 FutureTask 的对象
 * 5. 将 FutureTask 的对象作为参数传递到 Thread 类的结构器中,创立 Thread 对象,并调用 start 办法启动(通过 FutureTask 的对象调用办法 get 获取线程中的 call 的返回值)* 
 * */


// 实现 callable 接口的 call 办法
class NumThread implements Callable{

    private int sum=0;//

    // 能够抛出异样
    @Override
    public Object call() throws Exception {for(int i = 0;i<=100;i++){if(i % 2 == 0){System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {public static void main(String[] args){
        //new 一个实现 callable 接口的对象
        NumThread numThread = new NumThread();

        // 通过 futureTask 对象的 get 办法来接管 futureTask 的值
        FutureTask futureTask = new FutureTask(numThread);

        Thread t1 = new Thread(futureTask);
        t1.setName("线程 1");
        t1.start();

        try {
            //get 返回值即为 FutureTask 结构器参数 callable 实现类重写的 call 的返回值
           Object sum = futureTask.get();
           System.out.println(Thread.currentThread().getName()+":"+sum);
        } catch (ExecutionException e) {e.printStackTrace();
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

4、线程池

java 中常常须要用到多线程来解决一些业务,咱们十分不倡议单纯应用继承 Thread 或者实现 Runnable 接口的形式来创立线程,那样势必有创立及销毁线程消耗资源、线程上下文切换问题。同时创立过多的线程也可能引发资源耗尽的危险,这个时候引入线程池比拟正当,不便线程工作的治理。

应用线程池的形式:

背景:常常创立和销毁,使用量特地大的资源,比方并发状况下的线程,对性能影响很大。思路:提前创立好多个线程,放入线程池之,应用时间接获取,应用完放回池中。能够防止频繁创立销毁,实现反复利用。相似生存中的公共交通工具。(数据库连接池)益处:进步响应速度(缩小了创立新线程的工夫)升高资源耗费(反复利用线程池中线程,不须要每次都创立)便于线程治理
corePoolSize: 外围池的大小
maximumPoolSize: 最大线程数
keepAliveTime:线程没有工作时最多放弃多长时间后会终止

JDK 5.0 起提供了线程池相干 API:ExecutorService 和 Executors。ExecutorService: 真正的线程池接口,常见子类 ThreadPoolExecutor。void execute(Runnable coommand): 执行工作 / 命令,没有返回值,个别用来执行 Runnable。Futuresubmit(Callable task): 执行工作,有返回值,个别又来执行 Callable。void shutdown():敞开连接池。

线程池代码:

    ExecutorService e = Executors.newCachedThreadPool();
    ExecutorService e = Executors.newSingleThreadExecutor();
    ExecutorService e = Executors.newFixedThreadPool(3);
    // 第一种是可变大小线程池,依照工作数来调配线程,// 第二种是单线程池,相当于 FixedThreadPool(1)
    // 第三种是固定大小线程池。// 而后运行
    e.execute(new MyRunnableImpl());

5、JDK7 新增 Fork/Join 框架

6、JDK8 新增并行流

 并行流是 jdk8 的新个性之一,思维就是将一个程序执行的流变为一个并发的流,通过调用 parallel()办法来实现。并行流将一个流分成多个数据块,用不同的线程来解决不同的数据块的流,最初合并每个块数据流的处理结果。并行流默认应用的是公共线程池 ForkJoinPool,他的线程数是应用的默认值,依据机器的核数,咱们能够适当调整线程数的大小。线程数的调整通过以下形式来实现:System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "100");

例子:

public class Test4 {private static List<FileInfo> fileList= new ArrayList<FileInfo>();

    public static void main(String[] args) {System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "100");

           createFileInfo();

           long startTime=System.currentTimeMillis();

           fileList.parallelStream().forEach(e ->{

                    try {Thread.sleep(1);

                    } catch (InterruptedException f) {f.printStackTrace();

                    }

           });

           long endTime=System.currentTimeMillis();

           System.out.println("jdk8 并行流耗时:"+(endTime-startTime)+"ms");

    }

    private static void createFileInfo(){for(int i=0;i<30000;i++){fileList.add(new FileInfo("测试对象" + i));
           }

    }

}

四、怎么保障线程平安?

首先咱们先理解下,什么是线程平安?

线程平安问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没失去最新的数据,从而产生线程平安问题。而且线程平安须要听从三概念:1、原子性
    即一个操作或者多个操作 要么全副执行并且执行的过程不会被任何因素打断,要么就都不执行。2、可见性
    可见性是指当多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻看失去批改的值。3、有序性
    即程序执行的程序依照代码的先后顺序执行。

例如:一人上单人厕所,但没有上锁,这时第二人也进入,就导致难堪的事产生了。

怎么解决呢,有以下几种形式:

1、同步锁(synchronized 关键字)

1>、办法加锁

应用同步办法,对办法进行 synchronized 关键字润饰
将同步代码块提取进去成为一个办法,用 synchronized 关键字润饰此办法。对于 runnable 接口实现多线程,只须要将同步办法用 synchronized 润饰
而对于继承自 Thread 形式,须要将同步办法用 static 和 synchronized 润饰,因为对象不惟一(锁不惟一)。

2>、同步代码块(须要同步监视器,也就是锁做参)

原理是:当线程开始执行同步代码块前,必须先取得对同步代码块的锁定。并且任何时刻只能有一个线程能够取得对同步监视器的锁定,当同步代码块执行实现后,该线程会开释对该同步监视器的锁定。其中的锁,在非静态方法中可为 this,在静态方法中为以后类自身。

同步代码块:

synchronized(Object s){// 须要被同步的代码块}

留神,synchronized 能够润饰办法,润饰代码块,然而不能润饰结构器、成员变量等。

总结:

1. 同步办法依然波及到同步监视器,只是不须要咱们显示的申明。2. 非动态的同步办法,同步监视器是 this
3、动态的同步办法,同步监视器是以后类自身。继承自 Thread.class

2、Lock 类

lock: 在 java.util.concurrent 包内。共有三个实现:1、ReentrantLock
2、ReentrantReadWriteLock.ReadLock
3、ReentrantReadWriteLock.WriteLock
次要目标是和 synchronized 一样,两者都是为了解决同步问题,解决资源争端而产生的技术。性能相似但有一些以下区别:lock 更灵便,能够自在定义多把锁的加锁解锁程序(synchronized 要依照先加的后解程序)提供多种加锁计划:lock 阻塞式, 
trylock 无阻塞式, 
lockInterruptily 可打断式,还有 trylock 的带超时工夫版本。实质上和监视器锁(即 synchronized 是一样的)能力越大,责任越大,必须管制好加锁和解锁,否则会导致劫难。和 Condition 类的联合,性能会更高。

ReentrantLock:

步骤:1. 先 new 一个实例
    static ReentrantLock r=new ReentrantLock();
2. 加锁
    r.lock()或 r.lockInterruptibly();
    (此处也是个不同,后者可被打断。当 a 线程 lock 后,b 线程阻塞,此时如果是 lockInterruptibly,那么在调用 b.interrupt()之后,b 线程退出阻塞,并放弃对资源的争抢,进入 catch 块。(如果应用后者,必须 throw interruptable exception 或 catch))
3、开释锁
    r.unlock()(必须做!何为必须做呢,要放在 finally 外面。以避免异样跳出了失常流程,导致劫难。这里补充一个小知识点,finally 是能够信赖的:通过测试,哪怕是产生了 OutofMemoryError,finally 块中的语句执行也可能失去保障。)

ReentrantReadWriteLock:

可重入读写锁(读写锁的一个实现)ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();
两者都有 lock,unlock 办法。写写,写读互斥;读读不互斥。能够实现并发读的高效线程平安代码

优先应用程序:
Lock 类 > 同步代码块 > 同步办法

总结:Synchronized 与 lock 的异同?

雷同:二者都能够解决线程平安问题
不同:synchronized 机制在执行完相应的代码逻辑当前,主动的开释同步监视器
lock 须要手动的启动同步(lock()),同时完结同步也须要手动的实现(unlock())(同时以 lock 的形式更为灵便)

3、原子类(AtomicInteger、AtomicBoolean……)

首先先理解下什么是原子性:

如果把一个事务可看作是一个程序, 它要么残缺的被执行, 要么齐全不执行。这种个性就叫原子性

而应用原子类就能够保障原子性的操作,等同于 synchronized

Java 的原子类都寄存在并发包 java.util.concurrent.atomic 下


根本类型:

应用原子的形式更新根本类型
AtomicInteger:整形原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类

数组类型:

应用原子的形式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray:援用类型数组原子类

援用类型:

AtomicReference:援用类型原子类
AtomicStampedReference:原子更新援用类型里的字段原子类
AtomicMarkableReference:原子更新带有标记位的援用类型

对象的属性批改类型:

AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference:原子更新带有版本号的援用类型。该类将整数值与援用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决应用 CAS 进行原子更新时可能呈现的 ABA 问题。

4、volatile 关键字(只有在保障原子性的前提下才能够保障线程平安)

这个尽管是一个关键字,但波及很多,还是想详细描述下这段。
volatile 关键字作用:

1)保障了不同线程对这个变量进行操作时的可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是立刻可见的。2)禁止进行指令重排序。

针对作用的可见性,具体解释:

每个线程都有独立的高速缓存内存,在读取共享变量时,为了提高效率,防止频繁去物理内存读取值,都会读取之后存储到高速缓存内存,而后呢,这样就可能会呈现了线程平安问题,所以才有了线程平安的解决形式。那 volatile 是怎么解决的呢?1、读取形式更改:当共享变量 A 加上 volatile 关键字时,这个操作,就会通知所有线程,你们高速缓存区保留的共享变量 A,曾经有效了,你们只有去物理内存去读取才能够。2、批改值形式更改:用 volatile 关键字会强制线程将批改的值立刻写入物理内存。

针对作用的禁止指令重排序,具体解释:

 1)当程序执行到 volatile 变量的读操作或者写操作时,在其后面的操作的更改必定全副曾经进行,且后果曾经对前面的操作可见;在其前面的操作必定还没有进行;2)在进行指令优化时,不能将在对 volatile 变量拜访的语句放在其前面执行,也不能把 volatile 变量前面的语句放到其后面执行。

例如:

因为 flag 变量为 volatile 变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1、语句 2 后面,也不会讲语句 3 放到语句 4、语句 5 前面。然而要留神语句 1 和语句 2 的程序、语句 4 和语句 5 的程序是不作任何保障的。

并且 volatile 关键字能保障,执行到语句 3 时,语句 1 和语句 2 必然是执行结束了的,且语句 1 和语句 2 的执行后果对语句 3、语句 4、语句 5 是可见的。

讲到这里,怎么实现线程平安呢?
下面有讲到 volatile 能够保障可见性和被批改的变量的有序性,那能够实现原子性?
先看上面例子:

public class Test {
    public volatile int inc = 0;
     
    public void increase() {inc++;}
     
    public static void main(String[] args) {final Test test = new Test();
        for(int i=0;i<10;i++){new Thread(){public void run() {for(int j=0;j<1000;j++)
                        test.increase();};
            }.start();}
         
        while(Thread.activeCount()>1)  // 保障后面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

大家想一下这段程序的输入后果是多少?兴许有些敌人认为是 10000。然而事实上运行它会发现每次运行后果都不统一,都是一个小于 10000 的数字。

可能有的敌人就会有疑难,不对啊,下面是对变量 inc 进行自增操作,因为 volatile 保障了可见性,那么在每个线程中对 inc 自增完之后,在其余线程中都能看到批改后的值啊,所以有 10 个线程别离进行了 1000 次操作,那么最终 inc 的值应该是 1000*10=10000。

这外面就有一个误区了,volatile 关键字能保障可见性没有错,然而下面的程序错在没能保障原子性。可见性只能保障每次读取的是最新的值,然而 volatile 没方法保障对变量的操作的原子性。

首先咱们要晓得 自增操作是不具备原子性的 ,它包含读 取变量的原始值、进行加 1 操作、写入工作内存。那么就是说自增操作的三个子操作可能会宰割开执行,就有可能导致上面这种状况呈现:

如果某个时刻变量 inc 的值为 10,

线程 1 对变量进行自增操作,线程 1 先读取了变量 inc 的原始值,而后线程 1 被阻塞了;

而后线程 2 对变量进行自增操作,线程 2 也去读取变量 inc 的原始值,因为线程 1 只是对变量 inc 进行读取操作,而没有对变量进行批改操作,所以不会导致线程 2 的工作内存中缓存变量 inc 的缓存行有效,所以线程 2 会间接去主存读取 inc 的值,发现 inc 的值时 10,而后进行加 1 操作,并把 11 写入工作内存,最初写入物理内存。

而后线程 1 接着进行加 1 操作,因为曾经读取了 inc 的值,留神此时在线程 1 的工作内存中 inc 的值依然为 10,所以线程 1 对 inc 进行加 1 操作后 inc 的值为 11,而后将 11 写入工作内存,最初写入物理内存。

那么两个线程别离进行了一次自增操作后,inc 只减少了 1。

那是不是解决原子性,就能够实现线程平安了呢?
答案:是的。
有以下几种形式提供原子性,即可实现线程平安:
1、synchronized

public class Test {
    public  int inc = 0;
    
    public synchronized void increase() {inc++;}
    
    public static void main(String[] args) {final Test test = new Test();
        for(int i=0;i<10;i++){new Thread(){public void run() {for(int j=0;j<1000;j++)
                        test.increase();};
            }.start();}
        
        while(Thread.activeCount()>1)  // 保障后面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

2、采纳 Lock

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {lock.lock();
        try {inc++;} finally{lock.unlock();
        }
    }
    
    public static void main(String[] args) {final Test test = new Test();
        for(int i=0;i<10;i++){new Thread(){public void run() {for(int j=0;j<1000;j++)
                        test.increase();};
            }.start();}
        
        while(Thread.activeCount()>1)  // 保障后面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

3、采纳原子类 AtomicInteger:

public class Test {public  AtomicInteger inc = new AtomicInteger();
     
    public  void increase() {inc.getAndIncrement();
    }
    
    public static void main(String[] args) {final Test test = new Test();
        for(int i=0;i<10;i++){new Thread(){public void run() {for(int j=0;j<1000;j++)
                        test.increase();};
            }.start();}
        
        while(Thread.activeCount()>1)  // 保障后面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

在 java 1.5 的 java.util.concurrent.atomic 包下提供了一些原子操作类,即对根本数据类型的 自增(加 1 操作),自减(减 1 操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保障这些操作是原子性操作。atomic 是利用 CAS 来实现原子性操作的(Compare And Swap),CAS 实际上是利用处理器提供的 CMPXCHG 指令实现的,而处理器执行 CMPXCHG 指令是一个原子性操作。

五、线程是如何调度的呢?

调度策略:

工夫片:线程的调度采纳工夫片轮转的形式
抢占式:高优先级的线程抢占 CPU
Java 的调度办法:1. 对于同优先级的线程组成先进先出队列(先到先服务),应用工夫片策略
2. 对高优先级,应用优先调度的抢占式策略

线程的优先级:

等级:MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5

办法:

getPriority(): 返回线程优先级
setPriority(int newPriority): 扭转线程的优先级

留神:高优先级的线程要抢占低优先级的线程的 cpu 的执行权。不是肯定,仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完当前,低优先级的线程才执行。

六、线程分类?

1. 守护线程(是服务线程,程序运行时在后盾提供的一种通用服务的线程,如垃圾回收线程,异样解决线程)2. 用户线程(平时应用的用来解决逻辑的线程)若 JVM 中都是守护线程,以后 JVM 将退出。(形象了解,唇亡齿寒)

七、其它

java virtual machine(JVM):java 虚拟机内存构造:

正文完
 0