java多线程操作

2018-11-27  本文已影响0人  ZMRWEGo

概述

在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行

Java是最先支持多线程的开发的语言之一,Java从一开始就支持了多线程能力,因此Java开发者能常遇到上面描述的问题场景

一、相关概念

二、Java多线程

Java虚拟机允许应用程序并发地运行多个执行线程,常见的开启新的线程的方法主要有4种。

public class Main {

    public static void main(String[] args) {

       //将ThreadNew实例作为参数实例化Thread之后start启动线程
      //Thread构造器接收Runnable接口实例
        new Thread(new ThreadNew()).start();

        System.out.println(" Thread Main ");
    }
}

// 实现Runnable接口并在方法run里定义任务
class ThreadNew implements Runnable {

    @Override
    public void run() {

        try { // 延时0.5秒
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(" Thread New ");
    }
}

public class Main {

    public static void main(String[] args) {

        new ThreadNew2().start();

        System.out.println(" Thread Main ");
    }
}

// 继承自类Thread并重写run方法
class ThreadNew2 extends Thread {

    @Override
    public void run() {
        try { // 延时0.5秒
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" Thread New2 ");
    }
}
public class Main {

    public static void main(String[] args) {

        FutureTask<String> futureTask = new FutureTask<>(new ThreadNew3());

        new Thread(futureTask).start();

        System.out.println(" Thread Main ");

        try {
            System.out.println("执行结果是 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

// 实现接口Callable并在call()方法里定义任务
class ThreadNew3 implements Callable<String> {

    @Override
    public String call() throws Exception {

        try { // 延时0.5秒
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" Thread New3 ");

        return "Thread New3 Result";
    }
}

开启的新线程都有一个线程优先级,代表该线程的重要程度,可以通过Thread类的getPriority()和setPriority()来得到或者设置线程的优先级。线程的优先级范围是1~10,默认情况下是5。

在线程创建完成还未启动的时候,我们可以通过方法setDaemon()来将线程设置为守护线程。守护线程,简单理解为后台运行线程,比如当程序运行时播放背景音乐。守护线程与普通线程在写法上基本没有区别,需要注意的是,当进程中所有非守护线程已经结束或者退出的时候,即使还有守护线程在运行,进程仍然将结束。

  1. 线程自己在run()方法执行完后自动终止(安全的方式)
  2. 调用Thread.stop()方法强迫停止一个线程,不过此方法是不安全的,已经不再建议使用。(不安全方式)
  3. 比较安全可靠的是利用Java的中断机制,使用方法Thread.interrupt()。需要注意的是,通过中断并不能直接终止另一个线程,需要被中断的线程自己处理中断。被终止的线程一定要添加代码对isInterrupted状态进行处理,否则即使代码是死循环的情况下,线程也将永远不会结束。(安全方式)

三、锁机制

写法一、修饰在方法上

    public synchronized void add1() {

    }

写法二、修饰在代码块上

    public void add2() {
//这里的this指的是执行这段代码的对象
        synchronized (this) {

        }
    }

写法三、指定一个小的对象值进行加锁

    private byte[] lock = new byte[1];

    public void add3() {
        synchronized (lock) {

        }
    }

上面synchronized三种写法中,最后一种性能和执行效率最高,synchronized修饰方法上的效率最低。原因主要是作用在方法体上的话,即使获得了锁那么进入方法体内分配资源还是需要一定时间的。前两种锁的对象都是对象本身,加锁和释放锁都需要此对象的资源,那么自己造一个byte对象,可以提升效率。
关于sychronized的详细用法,可以查看这篇博文

Lock提供比synchronized更丰富,更灵活的锁操作。Lock的实现类比synchronized更灵活,但是必须手动释放和开启锁,适用于代码块锁,synchronized对象之间是互斥关系。

ReentrantLock是接口Lock的一个具体实现类。当许多线程视图访问ReentrantLock保护的共享资源时,JVM将花费较少的时间来调度线程,用更多的时间执行线程。它的用法主要如下:

class X {

    private final ReentrantLock lock = new ReentrantLock();

    public void m() {

        lock.lock();  // 开启锁

        try {
            //方法体
        } finally {
            lock.unlock();//释放锁
        }
    }
}
public class Counter {
 
    public volatile static int count = 0;
 
    public static void inc() {
 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
    }
 
    public static void main(String[] args) {
 
        //同时启动1000个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
 
        //这里每次运行的值都有可能不同,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }
}

许多人认为加入volatile关键字之后,我们得到的最终值会是1000,但实际上为Counter.count=992。
volatile的应用场景 https://blog.csdn.net/vking_wang/article/details/9982709


为什么会出现这种情况呢?

我们知道,在jvm中,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

这里可以用AtomicInteger来声明count,它通过CAS算法保证了线程的安全性

read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
但是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

四、线程池

Java通过Excutor提供4种线程池,分别为:

1. newCachedThreadPool

创建一个可缓存(可扩展)线程池,如果线程长度超过处理需求,可灵活回收空闲线程,若无可回收的,则新建线程

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        final int index = i;
    try {
        Thread.sleep(index * 1000);
    } 
        catch (InterruptedException e) {
            e.printStackTrace();
    }

cachedThreadPool.execute(new Runnable() {

@Override
public void run() {
    System.out.println(index);
}
});
}

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
从jconsole中,我们可以看到线程数后来在程序运行中维持不变


活动线程数.JPG

2. newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 10; i++) {
    final int index = i;

    fixedThreadPool.execute(new Runnable() {

@Override
public void run() {
try {
    System.out.println(index);
    Thread.sleep(2000);
} catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
}
});
}

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。

3. newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 scheduledThreadPool.schedule(new Runnable() {
  @Override
  public void run() {
      System.out.println("delay 3 seconds");
    }
  }, 3, TimeUnit.SECONDS);

表示延迟1秒后每3秒执行一次。
ScheduledExecutorService比Timer更安全,功能更强大

4. newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {

@Override
public void run() {
    try {
        System.out.println(index);
    Thread.sleep(2000);
} catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
        }
}
    });
}

结果依次输出,相当于顺序执行各个任务。

现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。


为什么要使用线程池:

  1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService
比较重要的几个类
ExecutorService: 真正的线程池接口。
ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

上一篇下一篇

猜你喜欢

热点阅读