黑马程序员技术交流社区

标题: 【南京校区】Java多线程与并发库高级应用 [打印本页]

作者: 大蓝鲸小蟀锅    时间: 2018-11-16 23:03
标题: 【南京校区】Java多线程与并发库高级应用
Java多线程与并发库高级应用
01. 传统线程技术回顾
传统是相对于JDK1.5而言的
        传统线程技术与JDK1.5的线程并发库
        线程就是程序的一条执行线索/线路。
创建线程的两种传统方式
1. 创建Thread的子类,覆盖其中的run方法,运行这个子类的start方法即可开启线程
Thread thread = new Thread()
        {        @Override
        public void run()
        {
        while (true)
        {
        获取当前线程对象                获取线程名字
        Thread.currentThread()                threadObj.getName()
        让线程暂停,休眠,此方法会抛出中断异常InterruptedException
        Thread.sleep(毫秒值);
}
}
};
                thread.start();
2. 创建Thread时传递一个实现Runnable接口的对象实例
Thread thread = new Thread(new Runnable()
{
        public void run()
        {}
});
                thread.start();
问题:下边的线程运行的是Thread子类中的方法还是实现Runnable接口类的方法
new Thread(
                        b、传递实现Runnable接口的对象
                        new Runnable()
                        {
                        public void run()
                        {}
}
){
                        a、覆盖Thread子类run方法
public void run(){}
}.start();
        分析:new Thread(Runnable.run()){run()}.start();
        子类run方法实际就是覆盖父类中的run方法,如果覆盖了就用子类的run方法,不会再找Runnable中的run方法了,所以运行的是子类中的run方法
总结:
Thread类中的run方法源代码中看出,两种传统创建线程的方式都是在调用Thread对象的run方法,如果Thread对象的run方法没有被覆盖,并且像上边的问题那样为Thread对象传递了一个Runnable对象,就会调用Runnable对象的run方法。
多线程并不一定会提高程序的运行效率。举例:一个人同时在三张桌子做馒头
多线程下载:并不是自己电脑快了,而是抢到更多服务器资源。例:服务器为一个客户分配一个20K的线程下载,你用多个线程,服务器以为是多个用户就分配了多个20K的资源给你。
02. 传统定时器技术回顾
传统定时器的创建:直接使用定时器类Timer
a、过多长时间后炸
new Timer().schedule(TimerTask定时任务, Date time定的时间);
b、过多长时间后炸,以后每隔多少时间再炸
new Timer().schedule(TimerTask定时任务, Long延迟(第一次执行)时间, Long间隔时间);
TimerTaskRunnable类似,有一个run方法
Timer是定时器对象,到时间后会触发炸弹(TimerTask)对象
示例:
new Timer().schedule(
new TimerTask()定时执行的任务
{
        public void run()
        {
        SOP(“bombing”);
}
显示计时信息
while (true)
{
        SOP(new Date().getSeconds());
        Thread.sleep(1000);
}
}
10        定好的延迟时间,10秒以后执行任务
);
问题:2秒后炸,爆炸后每隔3秒再炸一次
定时器2秒后炸,炸弹里还有定时器(每3秒炸一次)
class MyTimerTask extends TimerTask                这就是准备用的子母弹
{
        public void run()
        {
                本身就是一颗炸弹
                SOP(bombing);
                内部子弹
                new Timer().schedule(
new MyTimerTask(), 2000
);
        }
}
        放置子母弹,2秒后引爆
        new Timer().schedule(new MyTimerTask(), 2000);
问题延伸:
        上面的问题延伸,母弹炸过后,子弹每隔3秒炸一次,再每隔8秒炸一次
        1、在MyTimerTask内部定义一个静态变量记录炸弹号,在run方法内将炸弹号加1,每次产生新炸弹,号码就会加1,根据炸弹号判断是3秒炸还是8秒炸。
        注意:内部类中不能声明静态变量
        定义一个静态变量private static count = 0;
        run方法内部:count=count+1%2
        将定时器的时间设置为:2000+2000*count
        2、用两个炸弹来完成,A炸弹炸完后启动定时器安装B炸弹,B炸弹炸完后也启动一个定时器安装A炸弹。
定时器还可以设置具体时间,如某年某月某日某时……可以设置周一到周五做某事,自己设置的话需要换算日期时间,可以使用开源工具quartz来完成。
03. 传统线程互斥技术
        线程安全问题例子:银行转账
        同一个账户一边进行出账操作(自己交学费),另一边进行入账操作(别人给自己付款),线程不同步带来的安全问题
示例:逐个字符的方式打印字符串


class Outputer
{
        public void output(String name)
        {
        int len = name.length();
        for (int i=0; i<len; i++)
                SOP(name.charAt(i));逐个字符打印
        SOP();换行
}
}
public void test()
{
        Outputer outputer = new Outputer();
        new Thread(
new Runnable()
{
        public void run()
        {
                Thread.sleep(100);
        outputer.output(“zhangxiaoxiang”);
}
}).start();
        new Thread(
new Runnable()
{
        public void run()
        {
                Thread.sleep(100);
        outputer.output(“lihuoming”);
}
}).start();
}
要实现互斥,在这个位置必须使用同一个对象
使用name就达不到同步效果
使用output对象即可达到同步效果
要避免下边产生的问题,左边方法体中的代码要实现原子性
有一个线程正在使用这个方法的代码,别的线程就不能再使用。
就和厕所里的坑一样,已经有人在用了,别人就不能再去用了。
Java中某段代码要实现排他性,就将这段代码用synchronized关键字保护起来。
同步锁可以用任意对象,相当于门锁
synchronizedname
{
for (int i=0; i<len; i++)
        SOP(name.charAt(i));逐个字符打印
        SOP();换行
}
这样的话,有一个线程进入保护区域后,没出来的话,别的线程就不能进入保护区域。
注意:
内部类不能访问局部变量,要访问需加final
静态方法中不能创建内部类的实例对象
打印结果发现的问题:线程不同步所致,两个线程都在使用同一个对象
互斥方法:
        a、同步代码块
                synchronized (lock){}
        b、同步方法       
                方法返回值前加synchronized
                同步方法上边用的锁就是this对象
                静态同步方法使用的锁是该方法所在的class文件对象
使用synchronized关键字实现互斥,要保证同步的地方使用的是同一个锁对象
        public synchronized void output(String name)
        {
        int len = name.length();
        这里就不要再加同步了,加上极易出现死锁
        for (int i=0; i<len; i++)
                SOP(name.charAt(i));逐个字符打印
        SOP();换行
}
04. 传统线程同步通信技术
        面试题,子线程10次与主线程100次来回循环执行50
        下面是我刚看完面试题就暂停视频自己试着写的代码,还可以,结果完成要求了
在单次循环结束后让这个刚结束循环的线程休眠,保证另一个线程可以抢到执行权。
public class ThreadInterViewTest
{
        /**
         * 刚看到面试题没看答案之前试写
         * 子线程循环10次,回主线程循环100次,
         * 再到子线程循环10次,再回主线程循环100次
         * 如此循环50次         
         */
        public static void main(String[] args)
        {
                int num = 0;
                while (num++<50)
                {
                        new Thread(new Runnable()
                                        {
                                                @Override
                                                public void run()
                                                {
                                                        circle("子线程运行", 10);
                                                }
                                        }).start();
                        try
                        {
                                //加这句是保证上边的子线程先运行,刚开始没加,主线程就先开了
                                Thread.sleep(2000);
                        } catch (InterruptedException e)
                        {
                                e.printStackTrace();
                        }
                        circle("主线程", 100);       
                }
        }
       
        public static synchronized void circle(String name, int count)
        {
                for (int i=1; i<=count; i++)
                {
                        System.out.println(name+"::"+i);
                }
                try
                {
                        Thread.sleep(5000);
                } catch (InterruptedException e)
                {
                        e.printStackTrace();
                }
        }
}
1、将子线程和主线程中要同步的方法进行封装,加上同步关键字实现同步
2、两个线程间隔运行,添加一个标记变量进行比较以实现相互通信,加色的部分
wait   notify   notifyAll        wait会抛出异常
class Business
{
        private boolean bShouleSub = true;
        public synchronized void sub()
        {

                if (bShouleSub)
                {
                        for (int i=1; i<11; i++)
                        SOP(sub+i);
                bShouldSub = false;
                this.notify();
}
else
        this.wait();
}
        public synchronized void main()
        {
                if (!bShouldSub)
//此处使用while以增加程序健壮性,因为存在虚假唤醒,有时候并没有被notify就醒了。如果该方法没有同步的话,此处就更要使用while进行判断了,避免进程不同步问题
                {
                        for (int i=1; i<101; i++)
                        SOP(main+i);
                bShouldSub = true;
                this.notify();
}
else
        this.wait();
}
}

经验:要用到共同数据(包括同步锁)或相同算法的多个方法要封装在一个类中
        锁是上在代表要操作的资源类的内部方法中的,而不是上在线程代码中的。这样写出来的类就是天然同步的,只要使用的是同一个new出来的对象,那么这个对象就具有同步互斥特性
        判断唤醒等待标记时使用while增加程序健壮性,防止伪唤醒
05. 线程范围内共享变量的概念与作用
线程范围内共享数据图解:
代码演示:
class ThreadScopeShareData
{
        三个模块共享数据,主线程模块和AB模块
        private static int data = 0;        准备共享的数据
        存放各个线程对应的数据
        private Map<Thread, Integer> threadData = new HashMap<Thread, Integer>();
        public static void main(String[] args)
        {        创建两个线程
for (int i=0; i<2; i++)
{
        new Thread(
new Runnable()
{
        public void run()
        {现在当前线程中修改一下数据,给出修改信息
                data = new Random().nextInt();
                SOP(Thread.currentThread().getName()+将数据改为+data);
                将线程信息和对应数据存储起来
                threadData.put(Thread.currentThread(), data);
                使用两个不同的模块操作这个数据,看结果

                new A().get();
                new B().get();
}
}
).start();
}
}
        static class A
        {
        public void get()
        {
                data = threadData.get(Thread.currentThread());
        SOP(A+Thread.currentThread().getName()+拿到的数据+data);
}
}
        static class B
        {
        public void get()
        {
                data = threadData.get(Thread.currentThread());
        SOP(B+Thread.currentThread().getName()+拿到的数据+data);
}
}
}
结果并没与实现线程间的数据同步,两个线程使用的是同一个线程的数据。要解决这个问题,可以将每个线程用到的数据与对应的线程号存放到一个map集合中,使用数据时从这个集合中根据线程号获取对应线程的数据。代码实现:上面红色部分
程序中存在的问题:获取的数据与设置的数据不同步
                                 Thread-1共享数据设置为:-679705777777
                                Thread-1--A 模块数据:-679705777777
                                Thread-0共享数据设置为:11858818
                                Thread-0--A 模块数据:11858818
                                Thread-0--B 模块数据:-679705777777
                                Thread-1--B 模块数据:-679705777777
最好将Runnable中设置数据的方法也写在对应的模块中,与获取数据模块互斥,以保证数据同步
06. ThreadLocal类及应用技巧
        多个模块在同一个线程中运行时要共享同一份数据,实现线程范围内的数据共享可以用上一节中所用的方法。
        JDK1.5提供了ThreadLocal类来方便实现线程范围内的数据共享,它的作用就相当于上一节中的Map
        每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map集合中增加一条记录,key就是各自的线程,value就是各自的set方法传进去的值。
        在线程结束时可以调用ThreadLocal.clear()方法用来更快释放内存,也可以不调用,因为线程结束后也可以自动释放相关的ThreadLocal变量。
        一个ThreadLocal对象只能记录一个线程内部的一个共享变量,需要记录多个共享数据,可以创建多个ThreadLocal对象,或者将这些数据进行封装,将封装后的数据对象存入ThreadLocal对象中。
        将数据对象封装成单例,同时提供线程范围内的共享数据的设置和获取方法,提供已经封装好了的线程范围内的对象实例,使用时只需获取实例对象即可实现数据的线程范围内的共享,因为该对象已经是当前线程范围内的对象了。下边给出张老师的优雅代码:
package cn.itheima;
import java.util.Random;
public class ThreadLocalShareDataDemo
{        /**06. ThreadLocal类及应用技巧
         * 将线程范围内共享数据进行封装,封装到一个单独的数据类中,提供设置获取方法
         * 将该类单例化,提供获取实例对象的方法,获取到的实例对象是已经封装好的当前线程范围内的对象
         */
        public static void main(String[] args)
        {
                for (int i=0; i<2; i++)
                {
                        new Thread(
                                        new Runnable()
                                        {                                               
                                                public void run()
                                                {
                                                        int data = new Random().nextInt(889);
        System.out.println(Thread.currentThread().getName()+"产生数据:"+data);
                                                        MyData myData = MyData.getInstance();
                                                        myData.setAge(data);
                                                        myData.setName("Name:"+data);
                                                        new A().get();
                                                        new B().get();
                                                }
                                        }).start();
                }
        }
       
        static class A
        {        //可以直接使用获取到的线程范围内的对象实例调用相应方法
                String name = MyData.getInstance().getName();
                int age = MyData.getInstance().getAge();
                public void get()
                {
                        System.out.println(Thread.currentThread().getName()+"-- AA name:"+name+"...age:"+age);
                }
        }       
       
        static class B
        {
                //可以直接使用获取到的线程范围内的对象实例调用相应方法
                String name = MyData.getInstance().getName();
                int age = MyData.getInstance().getAge();
                public void get()
                {
                        System.out.println(Thread.currentThread().getName()+"-- BB name:"+name+"...age:"+age);
                }
        }       
       
        static class MyData
        {
                private String name;
                private int age;
                public String getName()
                {
                        return name;
                }
                public void setName(String name)
                {
                        this.name = name;
                }
                public int getAge()
                {
                        return age;
                }
                public void setAge(int age)
                {
                        this.age = age;
                }
                //单例
                private MyData() {};
                //提供获取实例方法
                public static MyData getInstance()
                {
                        //从当前线程范围内数据集中获取实例对象
                        MyData instance = threadLocal.get();
                        if (instance==null)
                        {
                                instance = new MyData();
                                threadLocal.set(instance);
                        }
                        return instance;
                }
                //将实例对象存入当前线程范围内数据集中
                static ThreadLocal<MyData> threadLocal = new ThreadLocal<MyData>();
        }
}
07. 多个线程之间共享数据的方式探讨
        例子:卖票:多个窗口同时卖这100张票,票就需要多个线程共享
a、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个对象中有共享数据。
卖票就可以这样做,每个窗口都在做卖票任务,卖的票都是同一个数据。
b、如果每个线程执行的代码不同,就需要使用不同的Runnable对象,有两种方式实现
Runnable对象之间的数据共享:
        a)将共享数据单独封装到一个对象中,同时在对象中提供操作这些共享数据的方法,可以方便实现对共享数据各项操作的互斥和通信。
        b)将各个Runnable对象作为某个类的内部类,共享数据作为外部类的成员变量,对共享数据的操作方法也在外部类中提供,以便实现互斥和通信,内部类的Runnable对象调用外部类中操作共享数据的方法即可。
        注意:要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
08Java5线程并发库的应用
        如果没有线程池,需要在run方法中不停判断,还有没有任务需要执行
        线程池的通俗比喻:接待客户,为每个客户都安排一个工作人员,接待完成后该工作人员就废掉。服务器每收到一个客户请求就为其分配一个线程提供服务,服务结束后销毁线程,不断创建、销毁线程,影响性能。
        线程池:先创建多个线程放在线程池中,当有任务需要执行时,从线程池中找一个空闲线程执行任务,任务完成后,并不销毁线程,而是返回线程池,等待新的任务安排。
        线程池编程中,任务是提交给整个线程池的,并不是提交给某个具体的线程,而是由线程池从中挑选一个空闲线程来运行任务。一个线程同时只能执行一个任务,可以同时向一个线程池提交多个任务。
线程池创建方法:
a、创建一个拥有固定线程数的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);       
        b、创建一个缓存线程池        线程池中的线程数根据任务多少自动增删 动态变化
        ExecutorService threadPool = Executors.newCacheThreadPool();
        c、创建一个只有一个线程的线程池  与单线程一样  但好处是保证池子里有一个线程,
当线程意外死亡,会自动产生一个替补线程,始终有一个线程存活
        ExecutorService threadPool = Executors.newSingleThreadExector();       
往线程池中添加任务
        threadPool.executor(Runnable)
关闭线程池:
        threadPool.shutdown()        线程全部空闲,没有任务就关闭线程池
        threadPool.shutdownNow()  不管任务有没有做完,都关掉
用线程池启动定时器:
        a、创建调度线程池,提交任务                延迟指定时间后执行任务
        Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间,时间单位);
        b、创建调度线程池,提交任务, 延迟指定时间执行任务后,间隔指定时间循环执行
        Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间,
间隔时间,时间单位);
        所有的 schedule 方法都接受相对 延迟和周期作为参数,而不是绝对的时间或日期。将以 Date 所表示的绝对时间转换成要求的形式很容易。例如,要安排在某个以后的 Date 运行,可以使用:schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
CallableFuture的应用:获取一个线程的运行结果
public interface Callable<V>
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。 Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
只有一个方法V call() 计算结果,如果无法计算结果,则抛出一个Exception异常。
使用方法:
        ExecutorService threadPool = Executors.newSingleThreadExccutor();
        如果不需要返回结果,就用executor方法  调用submit方法返回一个Future对象
        Future<T> future = threadPool.submit(new Callable<T>(){//接收一个Callable接口的实例对象
                        覆盖Callable接口中的call方法,抛出异常
                        public T call() throws Exception
                        {
                                ruturn T
}
});
获取Future接收的结果
futureget();会抛出异常
future.get()没有拿到结果就会一直等待
        Future取得的结果类型和Callable返回的结果类型必须一致,通过泛型实现。Callable要通过ExecutorServicesubmit方法提交,返回的Future对象可以取消任务。
public interface Future<V>
Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。
方法摘要
boolean
cancel(boolean mayInterruptIfRunning)           试图取消对此任务的执行。
V
get()           如有必要,等待计算完成,然后获取其结果。
V
get(long timeout, TimeUnit unit)           如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
boolean
isCancelled()           如果在任务正常完成前将其取消,则返回 true
boolean
isDone()           如果任务已完成,则返回 true
public interface CompletionService<V>
        CompletionService用于提交一组Callable任务,其take方法返回一个已完成的Callable任务对应的Future对象。好比同时种几块麦子等待收割,收割时哪块先熟先收哪块。
将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。
通常,CompletionService 依赖于一个单独的 Executor 来实际执行任务,在这种情况下,CompletionService 只管理一个内部完成队列。ExecutorCompletionService 类提供了此方法的一个实现。
CompletionService方法摘要
poll()           获取并移除表示下一个已完成任务的 Future,如果不存在这样的任务,则返回 null
poll(long timeout, TimeUnit unit)          获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则将等待指定的时间(如果有必要)。
submit(Callable<V> task)           提交要执行的值返回任务,并返回表示挂起的任务结果的 Future
submit(Runnable task, V result)           提交要执行的 Runnable 任务,并返回一个表示任务完成的 Future,可以提取或轮询此任务。
take()           获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。
ExecutorCompletionService构造方法摘要
ExecutorCompletionService(Executor executor)
          使用为执行基本任务而提供的执行程序创建一个 ExecutorCompletionService,并将 LinkedBlockingQueue 作为完成队列。
ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)
          使用为执行基本任务而提供的执行程序创建一个 ExecutorCompletionService,并将所提供的队列作为其完成队列。
示例:
ExecutorService threadPool = Executors.newFixedThreadPool(10);        //创建线程池,传递给coms
        threadPool执行任务,执行的任务返回结果都是整数
CompletionService<Integer> coms = new ExecutorCompletionService<Integer>(threadPool);
        提交10个任务  种麦子
for (int i=0; i<10; i++)
{
        final int num = i+1;
coms.submit(new Callable<Integer>(){
public Integer call()        覆盖call方法
{匿名内部类使用外部变量要用final修饰
        SOP(任务+num);
        Thread.sleep(new Random().nextInt(6)*1000);
        return num;
}
});
}
        等待收获        割麦子
for (int i=0; i<10; i++)
{        take获取第一个Future对象,用get获取结果
        SOP(coms.take().get());
}
java5的线程锁技术
java.util.concurrent.locks                 为锁和等待条件提供一个框架的接口和类,
接口摘要
Condition Object 监视器方法(waitnotifynotifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 setwait-set)。
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
ReadWriteLock 维护了一对相关的,一个用于只读操作,另一个用于写入操作。
类摘要
可以由线程以独占方式拥有的同步器。
long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本。
为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。
用来创建锁和其他同步类的基本线程阻塞原语。
一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
支持与 ReentrantLock 类似语义的 ReadWriteLock 实现。
ReentrantReadWriteLock.readLock() 方法返回的锁。
        Lock比传统线程模型中的synchronized更加面向对象,锁本身也是一个对象,两个线程执行的代码要实现同步互斥效果,就要使用同一个锁对象。锁要上在要操作的资源类的内部方法中,而不是线程代码中。
public interface Lock
所有已知实现类:
ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock
随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:
     Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally try-catch 加以保护,以确保在必要时释放锁。
方法摘要
void
lock()           获取锁。
void
lockInterruptibly()           如果当前线程未被中断,则获取锁。
newCondition()           返回绑定到此 Lock 实例的新 Condition 实例。
boolean
tryLock()           仅在调用时锁为空闲状态才获取该锁。
boolean
tryLock(long time, TimeUnit unit)           如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
void
unlock()           释放锁。
Locksynchronized对比,打印字符串例子







欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2