*多线程、同步

https://github.com/xu1211/JavaTest/tree/master/src/Thread/并发

目录

简介

线程的3种创建方式:

一:实现 Runnable 接口

二:继承 Thread 类本身

三:Callable 和 Future 创建线程

线程Thread类常用的方法:

创建线程的三种方式的对比

线程池

java自带线程池类ThreadPoolExecutor

线程安全

线程同步机制-锁:

方式一:同步代码块

方式二:同步函数

方法3:lock锁

线程同步机制-无锁:

CAS

线程通讯

原子性

线程的停止:

守护线程(后台线程):

线程死锁



简介

进程 : 正在执行的程序称作为一个进程。 进程负责了内存空间的划分。代表了内存中的执行区域。

线程: 线程在一个进程中负责了代码的执行,就是进程中一个执行路径,

多线程: 在一个进程中有多个线程同时在执行不同的任务。

一个java应用程序至少有两个线程, 一个是主线程负责main方法代码的执行,一个是垃圾回收器线程,负责了回收垃圾。

  •  新建状态/初始状态:

使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。


Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。默认优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

多线程的好处:

  1. 解决了一个进程能同时执行多个任务的问题。
  2. 提高了资源的利用率。


多线程的弊端:

  1. 增加cpu的负担。
  2. 降低了一个进程中线程的执行概率。
  3. 引发了线程安全问题。
  4. 出现了死锁现象。


线程的3种创建方式

一:实现 Runnable 接口

java.lang.Runnable它是一个接口,在它里面只声明了一个run()方法:run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。

  1. 自定义一个类实现Runnable接口
  2. 实现Runnable接口的run方法,把自定义线程的任务定义在run方法上。
  3. 创建Runnable实现类对象。    (Runnable实现类的对象并不是一个线程对象,只有是Thread或者是Thread的子类才是线程对象)
  4. 创建Thread类的对象,并且把Runnable实现类的对象作为实参传递。    (作用就是把Runnable实现类的对象的run方法作为了Thread对象的任务代码了)
  5. 调用Thread对象的start方法开启一个线程。

推荐使用:实现Runable接口。 原因: 因为java单继承,多实现。

class Demo2 implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i < 100 ; i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //-----------------------------------------------
        //自定义线程
        Demo2 runnable = new Demo2();
        //创建Thread类的对象, 把Runnable实现类对象作为实参传递。
        Thread thread = new Thread(runnable,"自定义线程");  //Thread类使用Target变量记录了runnable对象,
        //调用thread对象的start方法开启线程。
        thread.start();
        //-----------------------------------------------
        //main线程
        for(int i = 0 ; i < 100 ; i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }

}

控制台输出:
main:0
自定义线程:0
······99

二:继承 Thread 类本身

  1. 自定义一个类继承Thread类
  2. 重写Thread类的run方法 , 把自定义线程的任务代码写在run方法中    (每个线程都有自己的任务代码,jvm创建的主线程的任务代码就是main方法中的所有代码, 自定义线程的任务代码就写在run方法中,自定义线程负责了run方法中代码。)
  3. 创建Thread的子类对象,并且调用start方法开启线程。

    注意: 调用start方法一个线程就会开启,就会执行run方法中的代码run方法千万不能直接调用,直接调用run方法就相当调用了一个普通的方法而已并没有开启新的线程。

尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例

class Demo1 extends Thread {
    @Override  //把自定义线程的任务代码写在run方法中
    public void run() {
        for(int i  = 0 ; i < 100 ; i++){
            System.out.println("自定义线程:"+i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //-----------------------------------------------
        //自定义线程
        Demo1 thread = new Demo1();
        //调用start方法启动线程
        thread.start();
        //-----------------------------------------------
        //main线程
        for(int i  = 0 ; i < 100 ; i++){
            System.out.println("main线程:"+i);
        }
    }
}

控制台输出:
main线程:0
自定义线程:0
······99

三:Callable 和 Future 创建线程

继承Thread实现Runnable接口, 在执行完任务之后无法获取执行结果。

Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}


线程Thread类常用的方法

Thread(String name)      初始化线程的名字

setName(String name)      设置线程对象名

getName()      返回线程的名字

getPriority()      返回当前线程对象的优先级默认线程的优先级是5

setPriority(int newPriority)      设置线程的优先级虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大的优先级是10,最小的1,默认是5)。

public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

public void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

public final void setName(String name)
改变线程名称,使之与参数 name 相同。

public final void setPriority(int priority)
 更改线程的优先级。

public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。

public final void join(long millisec)
等待该线程终止的时间最长为 millis 毫秒。

public void interrupt()
中断线程。

public final boolean isAlive()
测试线程是否处于活动状态。

静态方法: 

public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。

public static void sleep(long millisec)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。

public static Thread currentThread()
返回对当前正在执行的线程对象的引用。

public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。

创建线程的三种方式的对比

  1.  采用1,3方法实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类
  2. 使用2方法继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程

线程池

创建线程不销毁,复用线程去执行其他任务

线程池的思路和生产者消费者模型是很接近的。
1. 准备一个任务容器
2. 一次性启动10个 消费者线程, 获取容器里的任务并执行
3. 刚开始任务容器是空的,所以线程都wait在上面。
4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
6. 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。

在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程

java中创建线程池的方式一般有两种:

  1. 通过new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)自定义创建
  2. 通过工具类java.util.concurrent.Executors工厂方法创建 , 封装了ThreadPoolExecutor

java自带线程池类ThreadPoolExecutor

ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)

corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。

maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;

unit : keepAliveTime 参数的时间单位。

workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

threadFactory :executor 创建新线程的时候会用到。

handler :饱和策略
    当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时
    1.ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
    2.ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。
    3.ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
    4.ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

例:

ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

第一个参数10 表示这个线程池初始化了10个线程在里面工作
第二个参数15 表示如果10个线程不够用了,就会自动增加到最多15个线程
第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过60秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就10个
第四个参数TimeUnit.SECONDS 如上
第五个参数 new LinkedBlockingQueue() 用来放任务的集合

execute方法用于添加新的任务

        threadPool.execute(new Runnable(){
            @Override
            public void run() {
                System.out.println("任务1");
            }
        });

Java通过Executors提供四种线程池,分别为:

  1. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

  • execute只能接受Runnable类型的任务
  • submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null

线程安全

比如说购票系统剩余1张票,100个线程在同时抢,有可能都抢到票票

出现线程安全问题的根本原因:

  1. 存在两个或者两个以上的线程对象共享着一个资源。
  2. 有两句及两句以上代码操作了共享资源。

sun提供了线程同步机制让我们解决这类问题的。

线程同步机制-锁

方式一:同步代码块

同步代码块的格式:

synchronized(锁对象){

    可能出现同步问题的代码...

}
//剩余票数
int ticket = 1; 

synchronized(锁对象){
    ticket = ticket - 1;
}

同步代码块注意事项

  1. 任意的一个对象都可以做为锁对象,但锁对象必须是多线程唯一的共享的对象,否则无效。
  2. 只有真正存在线程安全问题的时候才使用同步代码块,否则会降低效率。

方式二:同步函数

同步函数就是使用synchronized修饰一个函数。

同步函数注意事项

  1. 同步函数的锁是固定的:非静态的同步函数的锁对象是this对象,静态的同步函数的锁对象是当前函数所属的类的字节码文件(class对象)。

在同步代码块或者是同步函数中调用sleep方法不会释放锁对象,调用了wait方法会释放锁对象

同步代码块与同步函数 推荐使用: 同步代码块。

原因:

  1. 同步代码块的锁对象可以由我们随意指定,方便控制。同步函数的锁对象是固定的,不能由我们来指定。
  2. 同步代码块可以很方便控制需要被同步代码的范围,同步函数必须是整个函数的所有代码都被同步了。

方法3:lock锁

java.util.concurrent.locks.lock接口

void lock()  获取锁

boolean trylock()  尝试获取锁

void unlock()  释放锁

Lock和synchronized的区别:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。
  2. Lock可以trylock选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。
  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

线程同步机制-无锁

CAS

Atomic 原子类  (基于CAS思想)

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类
  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray :引用类型数组原子类
  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新引用类型里的字段原子类
  • AtomicMarkableReference :原子更新带有标记位的引用类型
  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

AtomicInteger 类常用方法

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。


线程通讯

线程同步那就需要线程之间通信,一个线程完成了自己的任务时,要通知另外一个线程去完成另外一个任务。

  • 使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法

wait():   等待, 如果线程执行了wait方法,那么该线程会进入一个以锁对象为标识符的线程池中等待,直到被其他持有了相同锁对象的线程调用notify方法才能唤醒。
notify():   唤醒, 唤醒以锁对象为标识符线程池等待线程中其中的一个。
notifyAll() :   唤醒线程池所有等待线程。

案例:消费者处于等待状态,生产者生产好之后唤醒消费者

wait与notify方法要注意事项

  1. wait方法与notify方法是属于Object对象的。    (锁可以使任意对象,所以可以被任意对象调用的方法定义在Object类中)
  2. wait方法与notify方法必须要在同步代码块或者是同步函数中才能使用。    (要对持有锁的线程操作,只有同步才具有锁。)
  3. wait方法与notify方法必需要由锁对象调用。

  • 使用lock方式进行线程交互,通过lock对象得到一个Condition对象, 用Condition对象的await, signal,signalAll 方法
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

//临时释放对象 lock, 并等待
condition.await();
//唤醒等待中的线程
condition.signal();

原子性

所谓的原子性操作即不可中断的操作,比如赋值操作

 i++ 这个行为,事实上是有3个原子性操作组成的。
步骤 1. 取 i 的值
步骤 2. i + 1
步骤 3. 把新的值赋予i

每一步都是一个原子操作,但是合在一起,就不是原子操作。就不是线程安全的。

JDK6 以后,新增加了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicInteger。
而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的。 换句话说,自增方法 incrementAndGet 是线程安全的,同一个时间,只有一个线程可以调用这个方法。


线程的停止:

  1. 停止一个线程一般都会通过一个变量去控制。
  2. 如果需要停止一个处于等待状态下的线程,那么我们需要通过变量配合notify方法或者interrupt()来使用。

interrupt()    把线程的等待状态强制清除,被清除状态的线程会接收到一个InterruptedException。


守护线程(后台线程):

在一个进程中如果只剩下了守护线程,那么守护线程也会死亡。线程默认都不是守护线程。

setDaemon(boolean on)    设置线程是否为守护线程,true为守护线程, false为非守护线程。

d.isDaemon()    判断线程是否为守护线程。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

join(): 加入(一个线程如果执行join语句,那么就有新的线程加入,执行该语句的线程必须要让步给新加入的线程先完成任务,然后才能继续执行)

当A线程执行到了B线程join方法时A就会等待,等B线程都执行完A才会执行,Join可以用来临时加入线程执行

案例:生产者消费者


线程死锁

java中同步机制解决了线程安全问题,但是也同时引发死锁现象

死锁现象出现的根本原因:

  1. 存在两个或者两个以上的线程。
  2. 存在两个或者两个以上的共享资源。

死锁现象的解决方案: 没有方案。只能尽量避免发生而已。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xyc1211

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值