并发与多线程

2019-12-23 08:56:39来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

并发与多线程

并发与多线程

基本概念

并发与并行

  1. 并发:指两个或多个事件在同一时间间隔内发生 。当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式称之为并发(Concurrent)
  2. 并行:指两个或者多个事件在同一时刻发生 。当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式称之为并行(Parallel)

进程与线程

  1. 一个程序可能有多个进程,一个进程由多个线程和共享资源组成
  2. 进程:拥有资源的基本单位
  3. 线程:独立调度分派的基本单位

线程

创建线程

Thread

  1. 继承 Thread 类(Thread 实现了 Runnable 接口)

  2. 重写 run 方法

  3. start( ) 方法启动线程

Runnable

  1. 实现 Runnable 接口
  2. 重写 run 方法
  3. new Thread(Runnable target),new Thread(Runnable target,String name)

多个 Thread 实例共用一个 Runnable,这些线程的 run 方法相同,可以共享相同的数据

但是存在线程同步问题

public class RunnableTest implements Runnable
{
    private int ticket = 10;
    public void run()
    {
        while (true)
        {
            if (ticket > 0)
            {
                System.out.println(Thread.currentThread().getName() + "售出" + ticket + "号票");
                ticket--;
            }
            else System.exit(0);
        }
    }
    public static void main(String[] args)
    {
        RunnableTest rt = new RunnableTest();
        Thread t1 = new Thread(rt, "1号窗口");
        Thread t2 = new Thread(rt, "2号窗口");
        t1.start();
        t2.start();
    }
}

print

1号窗口售出10号票
1号窗口售出9号票
1号窗口售出8号票
1号窗口售出7号票
2号窗口售出7号票
2号窗口售出5号票
1号窗口售出6号票
2号窗口售出4号票
1号窗口售出3号票
2号窗口售出2号票
1号窗口售出1号票

匿名类

匿名类可以方便的访问方法的局部变量,但是必须声明为 final,因为匿名类和普通局部变量生命周期不一致

jdk7 中已不再需要显示声明为 final,实际上被虚拟机自动隐式声明了

public static void main(String[] args)
{
    new Thread()
    {
        public void run()
        {
            //内容
        }
    }.start();
    new Thread(new Runnable()
    {
        public void run()
        {
            //内容
        }
	}).start();
}

Callable

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

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

  3. 实例化 FutuerTask 类,参数为 FutuerTask 接口实现类的对象来启动线程

  4. 通过 FutuerTask 类的对象的 get( ) 方法来获取线程结束后的返回值

    public class CallableTest implements Callable<Integer>
    {
        //重写执行体 call()
        public Integer call() throws Exception
        {
            int i = 0;
            for (; i < 10; i++)
            {
               //
            }
            return i;
        }
        public static void main(String[] args)
        {
            Callable call = new CallableTest();
            FutureTask<Integer> f = new FutureTask<Integer>(call);
            Thread t = new Thread(f);
            t.start();
            //得到返回值
            try
            {
                System.out.println("返回值:" + f.get());
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
    

    print

    返回值:10
    

线程方法

  1. 线程执行体:run( )

  2. 启动线程:start( )

  3. Thread 类方法

    方法 描述
    public final void setName(String name) 改变线程名称
    public final void setPriority(int priority) 设置优先级
    public final void setDaemon(boolean on) 设为守护线程,当只剩下守护线程时自动结束
    public final boolean isAlive( ) 测试线程是否处于活动状态
    public static void yield( ) 暂停当前线程(回到就绪状态)
    public static void sleep(long millisec) 进入休眠状态
    public final void join( ) 暂停当前线程,等待调用该方法线程执行完毕
    public final void join(long millisec) 暂停当前线程指定时间
    public static Thread currentThread() 返回对当前正在执行的线程对象的引用

线程状态

  1. 就绪状态:

    • start( ) 方法进入就绪状态,等待虚拟机调度
    • 运行状态调用 yield 方法会进入就绪状态
    • lock 池中的线程获得锁后进入就绪状态
  2. 运行状态:就绪状态经过线程调度进去运行状态

  3. 阻塞状态:

    • 休眠:调用 sleep 方法
    • 对象 wait 池:调用 wait 或 join 方法,被 notify 后进入 lock 池
    • 对象 lock 池:未获得锁
  4. 死亡状态:run 方法执行完毕

    graph TB T(新线程)--start方法-->A(就绪状态) A--线程调度-->B(运行状态) B--yield方法-->A B--sleep方法-->D(阻塞:休眠) B--wait或join方法-->E(阻塞:wait池) B--未获得锁-->F(阻塞:lock池) B--run方法执行完-->C(死亡状态) D--时间到-->A E--notify方法-->F F--获得锁-->A

线程同步

保证程序原子性、可见性、有序性的过程

阻塞同步

基于加锁争用的悲观并发策略

synchronized

  1. synchronized 含义

    • 使用 synchronized 可以锁住某一对象, 当其他线程也想锁住该对象以执行某段代码时,必须等待已经持有锁的线程释放锁

    • 释放锁的方式有互斥代码执行完毕、抛出异常、锁对象调用 wait 方法

  2. 不同的使用方式代表不同的锁粒度

    • 修饰普通方法 = synchronized(this)
    • 修饰静态方法 = synchronized(X.class)
    • 修饰代码块(对象 extends Object)

ReentrantLock

  1. 创建 Lock 锁

    ReentrantLock 实现了 Lock 接口, Lock lock = new ReentrantLock( )

  2. Lock 含义

    • 使用 lock( ) 方法表示当前线程占有 lock 对象

    • 释放该对象要显示掉用 unlock( ) 方法 ,多在 finally 块中进行释放

  3. trylock 方法

    • synchronized 会一直等待锁,而 Lock 提供了 trylock 方法,在指定时间内试图占用
    • 使用 trylock, 释放锁时要判断,若占用失败,unlock 会抛出异常
  4. Lock 的线程交互

    • 通过 lock 对象得到一个 Condition 对象,Condition condition = lock.newCondition( )

    • 调用这个Condition对象的:await,signal,signalAll 方法

  5. 示例

    public class LockTest
    {
        public static void log(String msg)//日志方法
        {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date date = new Date();
            String dateStr = sdf.format(date);
            System.out.println(dateStr + " " + Thread.currentThread().getName() + " " + msg);
        }
        public static void main(String[] args)
        {
            Lock lock = new ReentrantLock();
            new Thread("t1")
            {
                public void run()
                {
                    boolean flag = false;
                    try
                    {
                        log("线程已启动");
                        log("尝试占有lock");
                        flag = lock.tryLock(1, TimeUnit.SECONDS);
                        if (flag)
                        {
                            log("成功占有lock");
                            log("执行3秒业务操作");
                            Thread.sleep(3000);
                        }
                        else
                        {
                            log("经过1秒钟尝试,占有lock失败,放弃占有");
                        }
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    finally
                    {
                        if (flag)
                        {
                            log("释放lock");
                            lock.unlock();
                        }
                    }
                    log("线程结束");
                }
            }.start();
            try
            {
                //先让 t1 先执行两秒
                Thread.sleep(2000);
            }
            catch (InterruptedException e1)
            {
                e1.printStackTrace();
            }
            new Thread("t2")
            {
                public void run()
                {
                    boolean flag = false;
                    try
                    {
                        log("线程启动");
                        log("尝试占有lock");
    
                        flag = lock.tryLock(1, TimeUnit.SECONDS);
                        if (flag)
                        {
                            log("成功占有lock");
                            log("执行3秒的业务操作");
                            Thread.sleep(3000);
                        }
                        else
                        {
                            log("经过1秒钟的尝试,占有lock失败,放弃占有");
                        }
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    finally
                    {
                        if (flag)
                        {
                            log("释放lock");
                            lock.unlock();
                        }
                    }
                    log("线程结束");
                }
            }.start();
        }
    }
    

    print

    2019-11-07 15:50:01 t1 线程已启动
    2019-11-07 15:50:01 t1 尝试占有lock
    2019-11-07 15:50:01 t1 成功占有lock
    2019-11-07 15:50:01 t1 执行3秒业务操作
    2019-11-07 15:50:03 t2 线程启动
    2019-11-07 15:50:03 t2 尝试占有lock
    2019-11-07 15:50:04 t2 经过1秒钟的尝试,占有lock失败,放弃占有
    2019-11-07 15:50:04 t2 线程结束
    2019-11-07 15:50:04 t1 释放lock
    2019-11-07 15:50:04 t1 线程结束
    
  6. synchronized 和 Lock 区别

    • synchronized 是关键字,Lock 是接口, synchronized是内置的语言实现,Lock是代码层面的实现
    • synchronized 执行完毕自动释放锁,Lock 需要显示 unlock( )
    • synchronized 会一直等待,尝试占用锁,Lock 可以使用 trylock,在一段时间内尝试占用,时间到占用失败则放弃

非阻塞同步

非阻塞同步是一种基于冲突检测和数据更新的乐观并发策略

actomic 类

  1. 原子操作

    • 原子操作是不可中断的操作,必须一次性执行完成
    • 赋值操作是原子操作,但 a++ 不是原子操作, 而是取值、加一、赋值三个步骤
    • 一个线程取 i 的值后,还没来得及加一,第二个线程也来取值,就产生了线程安全问题
  2. actomic 类的使用

    • jdk6 以后,新增包 java.util.concurrent.atomic,里面有各种原子类,比如 AtomicInteger
    • AtomicInteger 提供了各种自增,自减等方法,这些方法都是原子性的。换句话说,自增方法 incrementAndGet 是线程安全的
    • 10000 个线程做 value 加一的操作,用 a++ 方式得出不准确的结果,用原子类 AtomicInteger 的 addAndGet( ) 方法得出正确结果
    public class ThreadTest
    {
        static int value1 = 0;
        static AtomicInteger value2 = new AtomicInteger(0);//原子整型类
        public static void main(String[] args)
        {
            for (int i = 0; i < 100000; i++)
            {
                new Thread()
                {
                    public void run()
                    {
                        value1++;
                    }
                }.start();
                new Thread()
                {
                    public void run()
                    {
                        value2.addAndGet(1);//value++的原子操作
                    }
                }.start();
            }
            while (Thread.activeCount() > 2)
            {
                Thread.yield();
            }
            System.out.println(value1);
            System.out.println(value2);
        }
    }
    

    print

    99996
    100000
    

无同步方案

如果一个方法不涉及共享数据,那么他天生就是线程安全的

可重入代码

可以在代码执行的任何时刻中断它,转而去执行另外一段代码,在控制权返回之后,原来的程序不会出现任何的错误

  1. 一个方法返回结果是可以预测的,输入了相同的数据,就能返回相同的结果,那这个方法就具有可重入性,也就是线程安全的

  2. 栈封闭是一种可重用代码

    多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量保存在虚拟机栈中,属于线程的私有区域,所以不会出现线程安全性

    public class ThreadTest
    {
        static void add()
        {
            int value = 0;
            for (int i = 0; i < 1000; i++)
            {
                value++;
            }
            System.out.println(value);
        }
    
        public static void main(String[] args)
        {
            ExecutorService threadPool = Executors.newCachedThreadPool();
            threadPool.execute(() -> add());
            threadPool.execute(() -> add());
            threadPool.shutdown();
        }
    }
    

    print

    1000
    1000
    

线程本地存储

  1. 把共享数据的可见范围限制在同一个线程之内,即便无同步也能做到避免数据争用

  2. 使用 java.lang.ThreadLocal 类来实现线程本地存储功能

    • ThreadLocal 变量是一个不同线程可以拥有不同值的变量,所有的线程可以共享一个ThreadLocal对象
    • 任意一个线程的 ThreadLocal 值发生变化,不会影响其他的线程
    • 用set()和get()方法对ThreadLocal变量进行赋值和查看其值
    public class ThreadLocalDemo
    {
        public static void main(String[] args)
        {
            ThreadLocal threadLocal1 = new ThreadLocal();
            Thread t1 = new Thread(() ->
            {
                threadLocal1.set(1);
                try
                {
                    Thread.sleep(3000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println(threadLocal1.get());
            });
            Thread t2 = new Thread(() -> threadLocal1.set(2));
            t1.start();
            t2.start();
        }
    }
    

    print

    1
    
  3. ThreadLocal 原理

    • 每个线程都有t一个 ThreadLocal.ThreadLocalMap 对象,调用 threadLocal1.set(T value) 方法时,将 threadLoacl1 和 value 键值对存入 map
    • ThreadLocalMap 底层数据结构可能导致内存泄露,尽可能在使用 ThreadLocal 后调用 remove( )方法

死锁

死锁条件

  1. 互斥条件
  2. 请求与保持条件
  3. 不可剥夺条件
  4. 循环等待条件(环路条件)

Java死锁示例

public static void main(String[] args)
{
    Object o1 = new Object();
    Object o2 = new Object();

    Thread t1 = new Thread()
    {
        public void run()
        {
            synchronized (o1)//占有 o1
            {
                System.out.println("t1 已占有 O1");
                try
                {
                    Thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o1
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println("t1 试图占有 o2");
                System.out.println("t1 等待中");
                synchronized (o2)
                {
                    System.out.println("t1 已占有 O2");
                }
            }
        }
    };
    Thread t2 = new Thread()
    {
        public void run()
        {
            synchronized (o2)  //占有 o2
            {
                System.out.println("t2 已占有 o2");
                try
                {
                    Thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o2
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println("t2 试图占有 o1");
                System.out.println("t2 等待中");
                synchronized (o1)
                {
                    System.out.println("t2 已占有 O1");
                }
            }
        }
    };
    t1.start();
    t2.start();
}

print

t1 已占有 O1
t2 已占有 o2
t1 试图占有 o2
t1 等待中
t2 试图占有 o1
t2 等待中

线程通信

  1. Object 类方法

    方法 描述
    wait( ) 线程进入等待池
    notify( ) 唤醒等待当前线程锁的线程
    notifyAll( ) 唤醒所有线程,优先级高的优先唤醒

    为什么这些方法设置在 Object 对象上?

    表面上看,因为任何对象都可以加锁

    底层上说,java 多线程同步的 Object Monitor 机制,每个对象上都设置有类似于集合的数据结构,储存当前获得锁的线程、等待获得锁的线程(lock set)、等待被唤醒的线程(wait set)

  2. 生产者消费者模型

    • sleep 方法,让出 cpu,但不放下锁
    • wait 方法,进入锁对象的等待池,放下锁
public class ProducerAndConsumer
{
    public static void main(String[] args)
    {
        Goods goods = new Goods();
        Thread producer = new Thread()//生产者线程
        {
            public void run()
            {
                while (true) goods.put();
            }
        };
        Thread consumer = new Thread()//消费者线程
        {
            public void run()
            {
                while (true) goods.take();
            }
        };
        consumer.start();
        producer.start();
    }
}
class Goods//商品类
{
    int num = 0;//商品数目
    int space = 10;//空位总数
    public synchronized void put()
    {
        if (num < space)//有空位可放,可以生产
        {
            num++;
            System.out.println("放入一个商品,现有" + num + "个商品," + (space - num) + "个空位");
            notify();//唤醒等待该锁的线程
        }
        else//无空位可放,等待空位
        {
            try
            {
                System.out.println("没有空位可放,等待拿出");
                wait();//进入该锁对象的等待池
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
    public synchronized void take()
    {
        if (num > 0)//有商品可拿
        {
            num--;
            System.out.println("拿出一个商品,现有" + num + "个商品," + (space - num) + "个空位");
            notify();//唤醒等待该锁的线程
        }
        else///等待生产产品
        {
            try
            {
                System.out.println("没有商品可拿,等待放入");
                wait();//进入该锁对象的等待池
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
}

print

没有商品可拿,等待放入
放入一个商品,现有1个商品,9个空位
放入一个商品,现有2个商品,8个空位
拿出一个商品,现有1个商品,9个空位
放入一个商品,现有2个商品,8个空位
放入一个商品,现有3个商品,7个空位
放入一个商品,现有4个商品,6个空位
拿出一个商品,现有3个商品,7个空位
放入一个商品,现有4个商品,6个空位
···

线程池

线程的启动和结束都是比较消耗时间和占用资源的,如果在系统中用到了很多的线程,大量的启动和结束动作会严重影响性能

线程池很像生产者消费者模式,消费的对象是一个一个的能够运行的任务

  1. 设计思路

    • 准备任务容器,可用 List,存放任务
    • 线程池类构造方法中创建多个执行者线程
    • 任务容器为空时,所有线程 wait
    • 当外部线程向任务容器加入任务,就会有执行者线程被 notify
    • 执行任务完毕后,没有接到新任务,就回归等待状态
  2. 实现一个线程池

    public class ThreadPool
    {
        int poolSize;// 线程池大小
        LinkedList<Runnable> tasks = new LinkedList<Runnable>();// 任务容器
        public ThreadPool(int poolSize)
        {
            this.poolSize = poolSize;
            synchronized (tasks)//启动 poolSize 个任务执行者线程
            {
                for (int i = 0; i < poolSize; i++)
                {
                    new ExecuteThread("执行者线程 " + i).start();
                }
            }
        }
        public void add(Runnable r)//添加任务
        {
            synchronized (tasks)
            {
                tasks.add(r);
                System.out.println("加入新任务");
                tasks.notifyAll();// 唤醒等待的任务执行者线程
            }
        }
        class ExecuteThread extends Thread//等待执行任务的线程
        {
            Runnable task;
            public ExecuteThread(String name)
            {
                super(name);
            }
            public void run()
            {
                System.out.println("启动:" + this.getName());
                while (true)
                {
                    synchronized (tasks)
                    {
                        while (tasks.isEmpty())
                        {
                            try
                            {
                                tasks.wait();
                            }
                            catch (InterruptedException e)
                            {
                                e.printStackTrace();
                            }
                        }
                        task = tasks.removeLast();
                        tasks.notifyAll(); // 允许添加任务的线程可以继续添加任务
                    }
                    System.out.println(this.getName() + " 接到任务");
                    task.run();//执行任务
                }
            }
        }
        public static void main(String[] args)
        {
            ThreadPool pool = new ThreadPool(3);
            for (int i = 0; i < 5; i++)
            {
                Runnable task = new Runnable()//创建任务
                {
                    public void run()//任务内容
                    {
                        System.out.println(Thread.currentThread().getName()+" 执行任务");
                    }
                };
                pool.add(task);//加入任务
                try
                {
                    Thread.sleep(1000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    print

    main 加入新任务
    启动:执行者线程 0
    执行者线程 0 接到任务
    执行者线程 0 执行任务
    启动:执行者线程 1
    启动:执行者线程 2
    main 加入新任务
    执行者线程 2 接到任务
    执行者线程 2 执行任务
    main 加入新任务
    执行者线程 2 接到任务
    执行者线程 2 执行任务
    
    
  3. java 线程池类

    • 默认线程池类 ThreadPoolExecutor 在 java.util.concurrent 包下

      ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
      /*
      第一个参数 int 类型, 10 表示这个线程池初始化了 10 个线程在里面工作
      第二个参数 int 类型, 15 表示如果 10 个线程不够用了,就会自动增加到最多 15个 线程
      第三个参数 60 结合第四个参数 TimeUnit.SECONDS,表示经过 60 秒,多出来的线程还没有接到任务,就会回收,最后保持池子里就 10 个
      第五个参数 BlockingQueue 类型,new LinkedBlockingQueue() 用来放任务的集合
      */
      
      
    • execute( ) 方法添加新任务

      public class TestThread 
      {   
          public static void main(String[] args) throws InterruptedException 
          {
              ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
              threadPool.execute(new Runnable()
              {//添加任务
                  public void run() 
                  {
                      System.out.println("执行任务");
                  }    
              });
          }
      }
      
      
  4. java 中几种线程池

    java 线程池的顶级接口是 Executor ,子接口是 ExecutorService ,子接口使用更广泛

    Executors 类提供了一系列工厂方法用于创建线程池,返回的线程池实现了 ExecutorService 接口

    • newCachedThreadPool有缓冲的线程池,线程数 JVM 控制,有线程可使用时不会创建新线程
    • newFixedThreadPool,固定大小的线程池,任务量超过线程数时,任务存入等待队列
    • newScheduledThreadPool,创建一个线程池,可安排在给定延迟后运行命令或者定期地执行
    • newSingleThreadExecutor,只有一个线程,顺序执行多个任务,若意外终止,则会新创建一个
    ExecutorService threadPool = null;
    threadPool = Executors.newCachedThreadPool();//缓冲线程池
    threadPool = Executors.newFixedThreadPool(3);//固定大小的线程池
    threadPool = Executors.newScheduledThreadPool(2);//定时任务线程池
    threadPool = Executors.newSingleThreadExecutor();//单线程的线程池
    threadPool = new ThreadPoolExecutor(···);//默认线程池,多个可控参数
    
    

线程安全类

  1. StringBuffer:内部方法用 synchronized 修饰
  2. Vetort:继承于 AbstractList
  3. Stack:继承于 Vector
  4. HashTable:继承于 Dictionary,实现了 Map 接口
  5. Property:继承于 HashTable,实现了 Map 接口
  6. concurrentHashMap:分段加锁机制

原文链接:https://www.cnblogs.com/pgjett/p/12082133.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:“死锁”四个必要条件的合理解释

下一篇:如何写出没有BUG的代码