多线程实践

Java 知识目录

创建多线程

继承 Thread 类

Thread 常用构造方法中,常用的有两个参数 Thread(Runnable target, String name) Runnable接口的实现类,以及线程名称

/**
 * 通过继承 Thread 创建多线程
 * 1. 创建一个Thread类的子类
 * 2. 在Thread子类中重写Thread类中的run方法,设置线程任务
 * 3. 调用Thread类中的start方法,开启新线程,执行run方法
 */
public class ExtendThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("ExtendThread" + i);
        }
    }

    /**
     * 运行结果
     * ExtendThread0
     * main0
     * ExtendThread1
     * main1
     * ExtendThread2
     * main2
     */
    public static void main(String[] args) {
        ExtendThread extendThread = new ExtendThread();
        extendThread.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("main" + i);
        }

    }
}

实现 Runnable 接口

/*
 * 创建`Runnable` 接口的实现类
 * 在实现类中重写 `Runnable` 接口的 `run` 方法,设置线程任务
 * 创建`Runnable` 接口的实现类对象
 * 创建`Thread`类对象,构造方法中传递`Runnable` 接口的实现类对象
 * 调用`Thread` 类中的 `start`方法,开启新的线程执行 `run`方法
 *

 * */
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();
        Thread thread = new Thread(runnable);

        thread.start();
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

Thread 的常用方法

package functions;

/**
*  static Thread currentThread() 		返回对当前正在执行的线程对象的引用。
*  String getName() 					返回此线程的名称。
*  void setName(String name) 			将此线程的名称更改为等于参数 name 。
*  static void sleep(long millis) 		使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),使用 Thread 直接调用即可
*  void start() 						导致此线程开始执行; Java虚拟机调用此线程的run方法。
 */
public class ThreadFunction implements Runnable{
    @Override
    public void run() {
        System.out.println("ThreadFunction:" + Thread.currentThread().getName());
        //延迟100ms
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = new ThreadFunction();
        Thread thread = new Thread(runnable);

        //添加了延时函数,所以main函数线程会先执行
        thread.start();

        System.out.println("main:" + Thread.currentThread().getName());
        //修改main函数线程的名称
        Thread.currentThread().setName("main2");
        System.out.println("main2:" + Thread.currentThread().getName());


    }
}

Thread 与 Runnable 的比较

实际开发中一般使用Runnable接口的方式比较多,因为:
通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类。而实现Runnable接口可以避免单继承的局限性。

匿名内部类的方式实现(不再叙述)

synchronized 关键字

  • synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所以现在的synchronized锁效率也优化得很不错了。

synchronized 使用方式

修饰一个代码块

  • 其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
/**
 * Synchronized 修饰一个代码块
 * 当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,
 * 在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块
 * 以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定
 * 当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
 */
public class SynchronizedBlock implements Runnable{

    private static int count = 0;//计数

    @Override
    public void run() {
        synchronized (this){
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }

    public static void main(String[] args) {

        SynchronizedBlock synchronizedBlock = new SynchronizedBlock();
        Thread thread1 = new Thread(synchronizedBlock,"SynchronizedBlock1");
        Thread thread2 = new Thread(synchronizedBlock,"SynchronizedBlock2");

        thread1.start();
        thread2.start();

    }
}

修饰一个方法

  • 其作用的范围是整个方法,作用的对象是调用这个方法的对象

修饰一个静态方法

  • 其作用的范围是整个静态方法,作用的对象是这个类的所有对象

修饰一个类

  • 其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象

volatile 关键字

  • 保证变量的可见性然后还有一一个作用是防止指令重排序。

synchronized关键字和volatile关键字比较

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
  • synchronized关键字 在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用synchronized关键字的场景还是更多-些。
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。

Actomic

  • Atomic是指一个 操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始, 就不会被其他线程干扰。所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
  • 基本类型
    • AtomicInteger:整形原子类
    • AtomicLong:长整型原子类
    • AtomicBoolean:布尔型原子类

**AtomicInteger **使用示例:

class AtomicIntegerTest {
	private AtomicInteger count = new AtomicInteger();
	//使⽤AtomicInteger之后,不需要对该⽅法加锁,也可以实现线程安全。
	public void increment() {
		count.incrementAndGet();
	}
	public int getCount() {
		return count.get();
	}
}

ThreadLocal

  • 其它三个关键字都是从线程外来保证变量的一致性,这样使得多个线程访问的变量具有一致性,解决资源共享的问题。而ThreadLocal的设计,是用来提供线程内的局部变量,这样每个线程都自己管理自己的局部变量,别的线程操作的数据不会对本线程产生影响。
 * @Author 安仔夏天很勤奋
 * Create Date is  2019/3/21
 *
 * 描述 Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。
 * 因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,
 * 它们也无法访问到对方的ThreadLocal变量。
 */
public class ThreadLocalExsample {/**
 * 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,
 * 并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,
 * 则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,
 * 因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
 */
 public static class MyRunnable implements Runnable {
 /**
 * 例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。
 * 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的
 * set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,
 * 他们仍然无法访问到对方的值。
 */
 private ThreadLocal threadLocal = new ThreadLocal();
 @Override
 public void run() {
 //一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
 threadLocal.set((int) (Math.random() * 100D));
 try {
 Thread.sleep(2000);
 } catch (InterruptedException e) {
 }
 //可以通过下面方法读取保存在ThreadLocal变量中的值
 System.out.println("-------threadLocal value-------"+threadLocal.get());
 }
 }public static void main(String[] args) {
 MyRunnable sharedRunnableInstance = new MyRunnable();
 Thread thread1 = new Thread(sharedRunnableInstance);
 Thread thread2 = new Thread(sharedRunnableInstance);
 thread1.start();
 thread2.start();
 }
}
​
运行结果
-------threadLocal value-------38
-------threadLocal value-------88

线程池

  • 线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

执行execute()方法和submit()方法的区别是什么呢?

1.execute()方 法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2. submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get (long t imeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

创建一个线程池—— ThreadPoolExecutor

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {
    //使⽤阿⾥巴巴推荐的创建线程池的⽅式
    //通过ThreadPoolExecutor构造函数⾃定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,     //最小可以同时运行的线程数量
                MAX_POOL_SIZE,      //线程最大连接数
                KEEP_ALIVE_TIME,    //回收等待时间为1L
                TimeUnit.SECONDS,   //等待时间单位为分钟
                new ArrayBlockingQueue<>(QUEUE_CAPACITY), //任务队列为ArrayBlockingQueue,并且容量为100;
                new ThreadPoolExecutor.CallerRunsPolicy());//饱和策略为CallerRunsPolicy
        for (int i = 0; i < 10; i++) {
            //创建WorkerThread对象(WorkerThread类实现了Runnable 接⼝)
            Runnable worker = new MyRunnable();
            //执⾏Runnable
            executor.execute(worker);
        }
        //终⽌线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

补充

并发编程的三个重要特性

  1. 原子性:一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized可以保证代码片段的原子性。
  2. 可见性:当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  3. 有序性:代码在执行的过程中的先后顺序,Java在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

参考文献:
https://www.cnblogs.com/fnlingnzb-learner/p/10335662.html
https://www.jianshu.com/p/6fc3bba12f38
https://blog.csdn.net/u010687392/article/details/50549236
https://github.com/Snailclimb/JavaGuide

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章