Java 并发 多线程 【还在学习】

接口Runnable和类Thread的区别

  • Runnable -> run(): 不是有单独的线程驱动的,需要依托其他线程
  • Thread -> run(): 具有自己的线程

使用Executor进行线程管理

  • 不占用当前启动线程的资源
  • 程序会在调用shutdown()之前提交的所有任务完成后结束
  • ExecutorService:

    • CachedThreadPool: 为每一个任务创建一个线程(一般用这个就好)
    • FixedThreadPool: 可以控制线程的数量
    • SingleThreadPool: 仅有一个线程,类似线程数量为1的FixedThreadPool
    • 范例:
    class LiftOff implements Runnable {
        private static int taskCount = 0;
        private final int id = taskCount++;
        @Override
        public void run() {
            for(int i=0; i<10; i++)
                System.out.println("["+id+"]("+Thread.currentThread().getId()+") " + i);
        }
        public static void main(String[] args) {
            ExecutorService exec = Executors.newCachedThreadPool();
            // ExecutorService exec = Executors.newFixedThreadPool(3); // 使用有限的线程集完成并发
            for(int i=0; i<5; i++)
                exec.execute(new LiftOff());
            // 防止新任务被提交给这个Executor
            exec.shutdown();
        }
    }

线程的一些基本操作

  • 休眠: Thread.sleep(milliseconds): 休眠一段时间,参数为毫秒
  • 优先级
    • 查看优先级: Thread.currentThread().getPriority()
    • 设置优先级: Thread.currentThread().setPriority()
    • 参数
      • Thread.MAX_PRIORITY 最高(10)
      • Thread.NORM_PRIORITY 中等(5)
      • Thread.MIN_PRIORITY 最低(1)
    • 注意: 尽管JDK有10个优先级,但它与多数操作系统都不能映射得很好。比如Windows7有7个优先级且不固定,所以这种映射关系也不是确定的。唯一可移植的方法是当调整优先级别的时候,只使用MAX_PRIORITYNORM_PRIORITYMIN_PRIORITY三种级别。
  • 让步: 使用Thread.yeild()进行暗示,申请可以将资源调度给其他线程使用,但系统未必会切换线程
  • 后台线程
    • 程序在运行时在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分
    • 当所有的非后台线程结束后,程序也就终止了,同时会杀死所有后台进程
    • 将线程设置为后台线程需要在线程启动前设置thread.setDaemo(true)
  • 加入一个线程: 如果在某个线程x中让线程t上调用t.join(),线程x将被挂起,直到线程t结束才恢复(即it.isAlive()返回为false)。对t.join()方法的调用可以被中断,做法为调用x.interrupt()。如果线程x被中断或正常结束,线程t也将和x一同结束
  • 在线程中捕获异常,默认会向外传播到外层

    • 使用Exector来管理线程的创建,在每个创建时附上一个异常处理器Thread.UncaughtExceptionHandler
    /** 测试方法 */
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool(new CacheExceptionThreadFactory());
        exec.execute(new Thread() {
            @Override
            public void run() {
                System.out.println("R线程名: " + getId());
                System.out.println("R: " + currentThread().getUncaughtExceptionHandler().toString());
                throw new RuntimeException("给你个异常瞧瞧");
            }
        });
        exec.shutdown();
    }
    /** 异常处理器 */
    class myUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("E线程名: " + t.getId() + "异常: " + e.getMessage());
        }
        @Override
        public String toString() {
            return "哈哈"+super.toString();
        }
    }
    /** 线程生成工厂 */
    class CacheExceptionThreadFactory implements ThreadFactory {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(new myUncaughtExceptionHandler());
            return t;
        }
    }

资源共享竞争

  • volatile: 修饰属性。保证数据在被修改后立即能写回内存,使得其他线程能读取到修改后的数据
  • synchronized: 修饰方法或语句块、。在上一个调用方法结束之前,其他调用该方法的线程全都被阻塞。
  • 使用Lock实现互斥机制,相比于synchronized的简洁性,显式使用Lock可以通过finally将系统维护在正确的状态,而不像synchronized出现错误后仅仅抛出一个异常。

    private Lock lock = new ReentrantLock();
    public void readOrWrite() {
        lock.lock();
        try{
            // some operators
            // return 必须出现在try{}中,确保unlock()不会过早发生,将数据暴露给下一个任务
            return;
        } finally {
            lock.unlock();
        }
    }
  • tryLock(long timeout, TimeUnit unit) 可以设置获取锁的时间,如果在设定的时间内无法获取锁,可以先进行其他操作。
  • 原子性: 对基本数据类型的读取和赋值操作被认为是安全的原子性操作。
  • 原子类:
    • AtomicInteger
    • AtomicLong
    • AtomicReference
  • 同步控制块:

    synchronized(syncObject) {
        // This code can be access by only task at a time
    }

自增线程安全性测试及解决方案

  • 原因: 多个线程同时访问共享变量i,而JVM允许每个线程存储变量的副本,i++的操作可以分为三步: 取值、自增、写回。存在一个线程在 自增 时,刚好有线程在 取值,因此最后会出现i增加的结果总比预计的结果线程小。
  • 测试例:
class TestIPlus {
    private int val = 0;
    public void run() {
        for(int i=0; i<10; i++) {
            this.val = 0;
            final CountDownLatch count = new CountDownLatch(10000);
            for(int j=0; j<100; j++) {
                new Thread(){
                    @Override
                    public void run() {
                        for(int i=0; i<100; i++) {
                            TestIPlus.this.val++;
                            count.countDown();
                        }
                    }
                }.start();
            }
            try {
                count.await();
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.val);
        }
    }
}
  • 使i++变得线程安全有3种方式:

    • 使用synchronized关键字,将i++写成一个方法,并使用synchronized修饰
    public synchronized void incI() {
        this.i++;
    }
    • 使用Lock,在修改i的位置加锁
    private Lock lock = new ReentrantLock();
    public void incI() {
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
        }
    }
    • 使用原子类AtomicInteger
    class TestIPlus {
        private AtomicInteger val;
        public void run() {
            for(int i=0; i<10; i++) {
                this.val = new AtomicInteger(0);
                final CountDownLatch count = new CountDownLatch(10000);
                for(int j=0; j<100; j++) {
                    new Thread() {
                        @Override
                        public void run() {
                            for(int i=0; i<100; i++) {
                                // 原子类自增
                                TestIPlus.this.val.getAndIncrement();
                                count.countDown();
                            }
                        }
                    }.start();
                }
                try {
                    count.await();
                } catch(InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.val);
            }
        }
    }

线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现。ThreadLocal对象通常当做静态域存储,在创建时,只能通过get()set()方法来访问该对象的内容。

class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> val = new ThreadLocal<Integer>() {
        private Random rand = new Random(47);
        protected Integer initialValue() {
            return rand.nextInt(1000);
        }
    };
    public static void increment() {
        val.set(val.get() + 1);
    }
    public static Integer getValue() {
        return val.get();
    }
}
class Accessor implements Runnable {
    private final int id;
    public Accessor(int id) {
        this.id = id;
    }
    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()) {
            ThreadLocalVariableHolder.increment();
            // 每个线程都有自己的val
            System.out.println(this);
        }
    }
    @Override
    public String toString() {
        return "#" + id + ": " + ThreadLocalVariableHolder.getValue();
    }
}

线程四种状态

  • 新建(New):当线程被创建时,它只会短暂的处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。
  • 就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配到时间片给线程,它就可以运行。
  • 阻塞(Blocked):线程能够运行,但有某个条件阻止了它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入就绪状态,它才有可能执行操作。
  • 死亡(Dead):处于死亡或终止状态的线程将不会再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

线程出现阻塞的原因

  • 调用sleep(millseconds)是任务进入休眠。
  • 调用wait()使线程挂起。直到线程得到notify()notifyAll()消息(或者在JavaSE5的java.util.concurrent类库中等价的signal()signalAll()消息)使线程进入就绪状态。
  • 任务在等待某个输入/输出完成。
  • 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获得了这个锁。

中断任务

猜你喜欢

转载自blog.csdn.net/goldlone/article/details/81132080
今日推荐