java并发编程系列:线程的基础概念


多线程并发是在java开发过程中比较重要的技术,从本节文章开始总结一些对java并发编程的一些学习心得和实战经验。


一、线程是什么?

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程是进程的一个执行路径,一个进程中至少有一个线程,进程中多个线程共享进程的资源。

二、线程实现方法

1.继承Thread类重写run方法

代码如下:

package cn.com.lingee.thread;

/**
 * 多线程实现方式一
 *
 * @author lingee
 * @date 2020-11-04
 */
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("子线程" + this.getName() + "启动...");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            MyThread myThread = new MyThread();
            myThread.setName("myThread-" + i);
            myThread.start();
        }
    }
}

2.实现Runable接口实现run方法

代码如下:

package cn.com.lingee.thread;

/**
 * 多线程实现方式二
 *
 * @author lingee
 * @date 2020-11-04
 */
public class RunableTest {

    static class MyRunable implements Runnable {

        @Override
        public void run() {
            System.out.println("子线程" + Thread.currentThread().getName() + "启动...");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new MyRunable());
            thread.setName("myThread-" + i);
            thread.start();
        }
    }
}

3.FutureTask创建任务,实现Callable接口的call()方法

代码如下:

package cn.com.lingee.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
 * 多线程实现方式三
 *  * @author lingee
 * @date 2020-11-04
 */
public class CallableTest {

    static class CallerTask implements Callable<String> {
        @Override
        public String call() {
            return "子线程" + Thread.currentThread().getName() + "运行成功";
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            FutureTask<String> future = new FutureTask<>(new CallerTask());
            new Thread(future).start();
            futures.add(future);
        }

        for (Future<String> future : futures) {
            while (true) {
                if (future.isDone() && !future.isCancelled()) {
                    String result = future.get();
                    System.out.println(result);
                    break;
                } else {
                    Thread.sleep(1000);
                }

            }
        }

    }
}

三、线程常用方法

  • Thread.currentThead():获取当前线程对象
  • getPriority():获取当前线程的优先级
  • setPriority():设置当前线程的优先级

注意:线程优先级高,被CPU调度的概率大,但不代表一定会运行,还有小概率运行优先级低的线程。

  • isAlive():判断线程是否处于活动状态 (线程调用start后,即处于活动状态)

  • join():调用join方法的线程强制执行,其他线程处于阻塞状态,等该线程执行完后,其他线程再执行。有可能被外界中断产生InterruptedException 中断异常。

  • sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态。

  • yield():调用yield方法的线程,会礼让其他线程先运行。(大概率其他线程先运行,小概率自己还会运行)

  • interrupt():中断线程

  • wait():导致线程等待,进入堵塞状态。该方法要在同步方法或者同步代码块中才使用的

  • notify():唤醒当前线程,进入运行状态。该方法要在同步方法或者同步代码块中才使用的

  • notifyAll():唤醒所有等待的线程。该方法要在同步方法或者同步代码块中才使用的

四、线程状态

  • NEW(线程创建状态)
  • RUNNABLE(线程就绪状态)
  • RUNNING(线程运行状态)
  • BLOCKED(线程阻塞状态)
  • TERMINATED(线程死亡状态)

生命周图:
线程生命周期图

1. 线程的NEW状态

当使用new关键字来创建一个线程对象时,在没有调用该对象的start方法之前,该线程并没有处于执行状态,因为线程没有启动,压根儿就不存在。和创建一个其他普通对象一样,只有通过调用start方法才能使线程进入到RUNNABLE状态。

2. 线程的RUNNABLE状态

只有调用线程对象的start方法之后,线程才会进入到该状态。但是进入到该状态的线程并不会立刻去执行。必须等到CPU调度,得到CPU分配的时间片之后,才可以执行,进入到RUNNING状态。所以在调用start方法之后,进入RUNNING之前的这段时间,线程状态为RUNNABLE状态,即:可执行状态。该状态下只能进入到RUNNING状态或者意外终止,不能进入到BLOCKED状态或者TERMINATED状态。

3. 线程的RUNNING状态

当处于RUNNABLE状态的线程得到CPU分配的时间片之后,才可以真正去执行线程内部的处理逻辑,处于RUNNING状态的线程,本身也是RUNNABLE的,但是处于RUNNABLE状态的线程不能也是RUNNING的。处于该状态下的线程,通过CPU调度或者中断等操作可以发生以下的状态转变:

  1. 调用线程的stop方法进入到TERMINATED状态
  2. 调用线程的sleep方法或者wait方法进入到BLOCKED状态
  3. 执行带阻塞的IO操作,进入到BLOCKED状态
  4. 进行锁竞争,进入到锁等待队列中,处于BLOCKED状态
  5. 由于受到CPU的调度,将时间片分配给了其他线程,则会进入到RUNNABLE状态
  6. 线程本身调用了yield方法,将CPU执行权放弃,进入到RUNNABLE状态

4. 线程的BLOCKED状态

线程由于锁竞争,IO阻塞,CPU调度或者其他原因进入到了BLOCKED状态之后,可以再次切换到以下几种状态:

  1. 调用stop()方法或者由于JVM意外崩溃而直接进入到TERMINATED状态
  2. 阻塞状态结束,比如IO操作完成之后,会再次进入到RUNNABLE状态
  3. 调用sleep之后,过了一段时间,休眠完成,会重新进入到RUNNALBE状态
  4. 调用wait方法之后,由于其他线程使用了notify或者notifyAll,将线程唤醒,此时会进入到RUNNABLE状态
  5. 锁竞争中,等待一段时间之后获取到了锁资源,进入到RUNNABLE状态

5. 线程的TERMINATED状态

如果线程进入到了TERMINATED状态之后,将没有机会再次进入到其他状态。因为该状态为线程生命周期中的最后一个状态,下面的几种情况将会使线程进入到TERMINATED状态:

  1. 线程生命周期正常结束,也就是线程中的执行逻辑正常结束
  2. 线程运行时出现异常意外结束
  3. JVM进程崩溃,会导致线程直接进入到TERMINATED状态

五、线程同步

因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

1.使用synchronized关键字

1.1同步方法

即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。示例:

package cn.com.lingee.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Synchronized测试类
 *
 * @author lingee
 * @date 2020-11-19
 */
public class SynchronizedTest {

    private int num = 0;

    public synchronized void testSync() {
        num++;
    }

    public static void main(String[] args) {
        SynchronizedTest syncTestMethod = new SynchronizedTest();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                syncTestMethod.testSync();
                System.out.println("同步方法:" + syncTestMethod.getNum());
            });
        }
    }

    public int getNum() {
        return num;
    }
}

输出结果:

同步方法:1
同步方法:2
同步方法:3
同步方法:4
同步方法:5
同步方法:6
同步方法:7
同步方法:8
同步方法:9
同步方法:10
1.2同步代码块

即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步,示例:

package cn.com.lingee.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Synchronized测试类
 *
 * @author lingee
 * @date 2020-11-19
 */
public class SynchronizedTest {

    Object obj = new Object();

    private int num = 0;

    public void testSyncBlock() {
        synchronized (obj) {
            num++;
        }
    }

    public static void main(String[] args) {
        SynchronizedTest syncTestMethod = new SynchronizedTest();
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        SynchronizedTest syncTestBlock = new SynchronizedTest();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                syncTestBlock.testSyncBlock();
                System.out.println("同步代码块:" + syncTestBlock.getNum());
            });
        }
    }

    public int getNum() {
        return num;
    }
}

输出结果:

同步代码块:1
同步代码块:3
同步代码块:6
同步代码块:2
同步代码块:8
同步代码块:9
同步代码块:7
同步代码块:5
同步代码块:4
同步代码块:10

2.lock接口

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。代码示例:

package cn.com.lingee.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * lock同步测试类
 *
 * @author lingee
 * @date 2020-11-20
 */
public class LockSyncTest {

    private int num = 0;

    public void testLock() {
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            num++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockSyncTest lockSyncTest = new LockSyncTest();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                lockSyncTest.testLock();
                System.out.println("lock方法:" + lockSyncTest.getNum());
            });
        }
    }

    public int getNum() {
        return num;
    }
}

输出结果:

lock方法:1
lock方法:2
lock方法:3
lock方法:4
lock方法:5
lock方法:6
lock方法:7
lock方法:8
lock方法:9
lock方法:10

ReentrantLock(可重入锁)是Lock的一个实现类,可以代替synchronized的功能,但是ReentrantLock需要手动释放锁,要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

3.使用volatile实现线程同步

volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。代码示例:

package cn.com.lingee.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * volatile同步测试类
 *
 * @author lingee
 * @date 2020-11-20
 */
public class VolatileSyncTest {

    private volatile int num = 0;

    public void testVolatile() {
        num++;
    }

    public static void main(String[] args) {
        VolatileSyncTest volatileSyncTest = new VolatileSyncTest();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                volatileSyncTest.testVolatile();
                System.out.println("volatile同步:" + volatileSyncTest.getNum());
            });
        }
    }

    public int getNum() {
        return num;
    }
}

输出结果:

volatile同步:1
volatile同步:2
volatile同步:3
volatile同步:4
volatile同步:5
volatile同步:6
volatile同步:7
volatile同步:8
volatile同步:9
volatile同步:10

4.使用原子变量实现线程同步

在JDK5新增并发编程包中有一个子包,java.util.concurrent.atomic,此包中定义了对单一变量执行原子操作的类。所有类都有get和set方法,工作方法和对volatile变量的读取和写入一样。此包提供了原子变量的9种风格(AtomicInteger,AtomicLong,AtomicReference、AtomicBoolean,原子整型,长型,引用、及原子标记引用和戳记引用类的数组形式,其原子地更新一对值)原子变量类共有12个,分成4组:计量器,域更新器,数组以及复合变量。最常用的原子变量是计量器:AtomicInteger,AtomicLong,AtomicBoolean及AtomicReference。代码示例:

package cn.com.lingee.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 原子类同步测试类
 *
 * @author lingee
 * @date 2020-11-20
 */
public class AtomicSyncTest {

    private AtomicInteger num = new AtomicInteger();

    public int testAtomic() {
        return num.addAndGet(1);
    }

    public static void main(String[] args) {
        AtomicSyncTest atomicSyncTest = new AtomicSyncTest();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                System.out.println("原子类同步:" + atomicSyncTest.testAtomic());
            });
        }
    }
}

结果输出:

原子类同步:1
原子类同步:2
原子类同步:3
原子类同步:4
原子类同步:5
原子类同步:6
原子类同步:7
原子类同步:8
原子类同步:9
原子类同步:10

5.使用阻塞队列实现线程同步

上面关于同步的实现方式是Java并发程序设计基础的底层构建块,在实际的编程使用中,使用较高层次的类库会相对安全方便。对于典型的生产者和消费者问题,可以使用阻塞队列解决,这样就不用考虑锁和条件的问题了。
java.util.concurrent包提供了几种形式的阻塞队列:

  • LinkedBlockingQueue:无容量限制,链表实现;
  • LinkedBlockingDeque:双向队列,链表实现;
  • ArrayBlockingQueue:需指定容量,可指定公平性,循环数组实现;
  • PriorityBlockingQueue:无边界优先队列,用堆实现。
    代码示例:
package cn.com.lingee.sync;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 阻塞队列同步测试类
 *
 * @author lingee
 * @date 2020-11-20
 */
public class BlockQueueSyncTest {

    private int num = 0;

    BlockingQueue blockingQueue = new ArrayBlockingQueue(10);

    public void testBlockQueue() {
        try {
            if (null != blockingQueue.poll()) {
                num = (int) blockingQueue.take();
            }
            blockingQueue.offer(++num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        BlockQueueSyncTest blockQueueSyncTest = new BlockQueueSyncTest();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                blockQueueSyncTest.testBlockQueue();
                try {
                    System.out.println("阻塞队列同步:" + blockQueueSyncTest.getBlockingQueue().take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }

    public BlockingQueue getBlockingQueue() {
        return blockingQueue;
    }
}

输出结果:

阻塞队列同步:1
阻塞队列同步:2
阻塞队列同步:3
阻塞队列同步:4
阻塞队列同步:5
阻塞队列同步:6
阻塞队列同步:7
阻塞队列同步:8
阻塞队列同步:9
阻塞队列同步:10

总结

以上就是本篇文章要讲的内容,本文从线程的定义、线程实现方式、线程常用方法、线程状态迁移、线程同步的五种方法来介绍了线程的基本概念。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值