A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

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
所有已知实现类:
随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 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对比,打印字符串例子


0 个回复

您需要登录后才可以回帖 登录 | 加入黑马