1

目录

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():关闭连接池。

image.png

线程池代码:

    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 下

image.png
基本类型:

使用原子的方式更新基本类型
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变量后面的语句放到其前面执行。

例如:
image.png
由于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虚拟机内存结构:
image.png


稳之楠
130 声望26 粉丝

行之稳,为之楠!