Java多线程并发

多线程的优点

  • 提高CPU利用率
  • 减少响应时间
  • 进程切换消耗远大于线程切换

多线程的代价

  • 设计更复杂
  • 上下文切换的开销,程序执行速度变慢
  • 增加资源消耗

并发编程模型

并行工作者模型

在并行工作者模型中

  • 委派者(Delegator)将传入的作业分配给不同的工作者。
  • 每个工作者完成整个任务。
  • 工作者们并行运作在不同的线程上,甚至可能在不同的CPU上。

缺点

共享状态可能会很复杂

  • 线程需要避免竞态,死锁以及很多其他共享状态的并发性问题。
  • 在等待访问共享数据结构时,线程之间的互相等待将会丢失部分并行性。

无状态的工作者

  • 共享状态能够被系统中的其他线程修改。
  • 工作者在每次需要的时候必须重读状态,以确保每次都能访问到最新的副本,工作者无法在内部保存这个状态(但是每次需要的时候可以重读)称为无状态的。

任务顺序不确定


流水线模式

  • 每个工作者只负责作业中的部分工作。
  • 当完成了自己的这部分工作时工作者会将作业转发给下一个工作者。
  • 每个工作者在自己的线程中运行,并且不会和其他工作者共享状态。
  • 有时也被成为无共享并行模型
  • 通常使用非阻塞的IO来设计使用流水线并发模型的系统

Actors模型

  • 在Actor模型中每个工作者被称为actor。
  • Actor之间可以直接异步地发送和处理消息。
  • Actor可以被用来实现一个或多个像前文描述的那样的作业处理流水线。

Channel模型

  • 工作者之间不直接进行通信,在不同的通道中发布自己的消息(事件)。
  • 其他工作者们可以在这些通道上监听消息,发送者无需知道谁在监听。

优点

  • 无需共享的状态
  • 有状态的工作者
  • 合理的作业顺序

缺点

  • 作业的执行往往分布到多个工作者上,作业执行难以被追踪。
  • 嵌入过多的回调处理,往往会出现回调地狱(callback hell)现象
  • 加大了代码编写的难度

函数式并行

  • 函数式并行的基本思想是采用函数调用实现程序。
  • 函数之间可以像流水线模型那样互相发送消息。
  • 某个函数调用另一个函数,这个过程类似于消息发送。

Java线程

Java8中,加入函数式接口:只有一个函数的接口

用该注解表明@FunctionalInterface

如Runnable 接口、Callable接口

Runnable 接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

class Thread implements Runnable {
        ....
}

Callable接口

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

运行Callable任务可以拿到一个Future对象,表示异步计算的结果。
它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。


Java 提供了三种创建线程的方法:

1.通过实现 Runnable 接口
MyRunnable类并没有Thread 的方法
可以通过以下方式得到当前线程的引用

Thread.currentThread();
String threadName = Thread.currentThread().getName();

2.通过继承 Thread 类本身

3.通过 Callable 和 Future 创建线程

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

实例:

新建无参线程
Thread t = new Thread();

调用无参构造函数

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

调用初始化函数

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

分析:

  • ThreadGroup会递归去调用父类的getThreadGroup来进行初始化,
  • 通过ThreadGroup调用checkAccess()方法来检查当前线程是否有权限操作此线程.
  • Thread类的daemon,priority属性会由父类继承.

如果name为空,抛出空指针异常

把当前线程作为新线程的父进程

获取SecurityManager安全管理器
如果线程组为空,

  • 如果SecurityManager不为空,取SecurityManager的线程组
  • 如果SecurityManager为空,取父进程的线程组

检查可达性
checkPermission()

  • AccessController的核心方法,这个方法决定一个特定的操作能否被允许.

往线程组添加新线程但未启动

    void addUnstarted() {
        synchronized(this) {
            if (destroyed) {
                throw new IllegalThreadStateException();
            }
            nUnstartedThreads++;
        }
    }

设置线程组
设置是否为守护线程 ,如果父线程是,新线程也是
设置优先级为父进程优先级
设置栈大小
设置线程ID

启动线程

public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

如果线程状态不为0 抛出异常
线程组添加线程,未开始线程数量 - -,开始线程数量++
调用本地方法start0启动线程
如果调用start0失败,线程组移除该线程,未开始线程数量 ++,开始线程数量 - -

运行线程
start()之后,自动执行run()

  • 实现Runnable则调用Runnable的run方法
  • 未实现则调用自己的run方法
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

线程退出

    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

如果线程组不为空,调用线程组的终止方法,重置各种参数


竞态条件与临界区

  • 当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件
  • 导致竞态条件发生的代码区称作临界区
  • 在临界区中使用适当的同步就可以避免竞态条件。

线程安全与共享资源.

  • 允许被多个线程同时执行的代码称作线程安全的代码。
  • 线程安全的代码不包含竞态条件。
  • 当多个线程同时更新共享资源时会引发竞态条件。

局部变量

  • 局部变量存储在线程自己的栈中。
  • 永远也不会被多个线程共享。

局部的对象引用
- 引用本身没有被共享,但引用所指的对象并没有存储在共享堆
- 如果在某个方法中创建的对象不会逃逸出那么它就是线程安全的。

–参考笔记逃逸分析

对象成员

  • 对象成员存储在堆上。
  • 如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。
  • 非线程安全的对象仍可以通过创建各自对象来消除竞态条件。

线程控制逃逸规则

如果一个资源的创建,使用,销毁都在同一个线程内完成,
且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
资源可以是对象,数组,文件,数据库连接,套接字等等。

即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),就不再是线程安全的。


线程安全及不可变性

  • 多个线程同时读同一个资源不会产生竞态条件。
  • 可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。
  • “不变”(Immutable)和“只读”(Read Only)是不同的。
  • 当一个变量是“只读”时,变量的值不能直接改变,但是可以在其它变量发生改变的时候发生改变。

Java内存模型

Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。
Java内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

Java内存模型把Java虚拟机内部划分为线程栈

  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。
  • 如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝

问题:

  • 线程对共享变量修改的可见性
  • 当读,写和检查共享变量时出现race conditions

解决办法:

Java 同步块包括方法同步和代码块同步。
volatile声明


锁池和等待池

在java中,每个对象都有两个池,锁(monitor)池和等待池

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),
由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。
如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。
如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.


线程通信

1、通过共享对象通信

在共享对象的变量里设置信号值。

2、忙等待(Busy Wait)

准备处理数据的线程正在等待数据变为可用。
等待线程运行在一个循环里

3、wait(),notify()和notifyAll()

一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态

为了调用wait()或者notify(),线程必须先获得那个对象的

class MyRunnable1 implements Runnable {
    public void run() {
        synchronized (Test.class) {
            System.out.println("1 start ");
            try {
                System.out.println("sleep 3s");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("notify a thread");
            Test.class.notify();
            //唤醒当前对象上的等待线程
            //通知对象的等待池里的线程使一个线程进入锁定池
            System.out.println("1 is over ");
        }
    }
}
Thread t1 = new Thread(new MyRunnable1());
        //当前线程锁定Test.class
        synchronized (Test.class) {
            t1.start();
            Thread.sleep(3000);
            //放弃对象锁,进入等待Test.class的等待锁定池
            System.out.println("wait for Test.class");
            Test.class.wait();
            System.out.println("I am notified");
        }
        Thread.sleep(1000);
        System.out.println("main over");
  • 调用wait时候JVM首先要检查下当前线程是否是的拥有者
  • 如果没有持有对象锁,会抛出IllegalMonitorStateException异常
  • 即在对象同步块里调用wait()和notify()
  • 一旦一个线程被唤醒,不能立刻就退出wait()的方法调用, 直到调用notify()的线程退出了它自己的同步块。

如果在MyRunnable1 run()里添加

while(true);

main将永远无法被唤醒,因为锁将一直被MyRunnable1 持有
每个线程在退出wait()前必须获得对象的锁

问题1:
丢失的信号:notify()和notifyAll()方法不会保存调用它们的方法,因为当这两个方法被调用时,有可能没有线程处于等待状态。
为了避免丢失信号,必须把它们保存在信号类里。
如添加一个变量标识信号。


ThreadLocal类

  • ThreadLocal并不是一个Thread,而是Thread的局部变量
  • ThreadLocal实例通常来说都是private static类型。
  • ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本
  • 每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
  • 主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问
    如: 网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息
class MyRunnable2 implements Runnable {
    ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    public void run() {
        threadLocal.set((int) (Math.random() * 1000D));
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(threadLocal.get());
    }
}
MyRunnable2 t = new MyRunnable2();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();

ThreadGroup

一个线程归属到一个线程组之中后,就不能再更换其所在的线程组。
方便统一管理,线程组可以进行复制,快速定位到一个线程,统一进行异常设置等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值