多线程在我操作系统的博客中也有涉及,而这里我们讲一下java中的多线程实现。
先回顾一下,
抢占式:随时能够中断另一个任务。
非抢占式:只有一个任务同意被中断时才能被中断。会导致死锁。
多线程:共享变量因此便于通信,创建开销少。
多进程:相互独立,通信有共享内存和消息传递,创建开销很大。
java的GUI用一个单独的线程在后台收集用户界面的事件。
因此repaint调用时,会把这个事件发送到事件请求队列中,执行完前面的事件,才会执行repaint事件。
java有一个线程在后台垃圾回收。
Thread.sleep(int i); 线程睡眠i毫秒。
java多线程实现方法:
实现Runnable接口,实现run方法
(1)继承Runnable并实现public void run();
(2)Thread t=new Thread(Runnable r);
(3)t.start();自动调用run方法,执行完就返回。
继承Thread方法,覆写run方法
runnable能够实现资源共享。
t.interrupt()请求中断一个线程,线程的中断状态位置位.当线程在阻塞时被请求中断,则抛出异常并被终止,特别在sleep()这个阻塞调用中,如果有请求中断,则会清除中断状态位。
Thread.currentThread().isInterrupted()判断中断状态位是否被置位。
线程状态:
1.new。Thread t=new Thread()时。
2.runnable。t.start()时。注意只是可运行的,因此可以运行也可以没有运行,因为在线程运行时会有中断发生。
3.blocked。当(1)sleep()(2)I/O中断(3)申请锁但是没有得到。当被阻塞时,CPU可以运行另外的线程。
4.dead。当(1)run正常返回(2)run由于异常返回。
t.isAlive()判断是否是可运行的或阻塞的。
优先级:
线程调度器只是把优先级看作一个参考因素,而不是全部。
t.setPriority(int );
static yield() 当前线程让步,选择其他线程中优先级最高的。
t.setDaemon(boolean) 标记为守护线程。
守护线程:为其他线程提供服务,当只有守护线程时,则程序退出。
线程组:ThreadGroup。根据功能进行分类,比如有多个线程是用来下载图片的,当用户请求中断时,这些线程是一起中断的,为了操作方便,可以把他们归在一个线程组。
ThreadGroup g=new ThreadGroup("....");
Thread t1=new Thread(g,"...");
Thread t2=new Thread(g,"...");
g.interrupt(); 中断t1和t2
g.activeCount(); g这个线程组中可运行状态的线程数量
t1.getThreadGroup();返回t1所属的线程组。
未捕获异常处理器:
当在run方法发生异常时,如果你try-catch了,则会捕获了,如果你throws了,则会抛出,但是如果发生了那些你没有预料到的异常,则会发送给未捕获异常处理器,然后线程终止。
(1)Thread.UncaughtExceptionHandler接口是未捕获异常处理器,必须实现uncaughtException(Thread,Throwable);
(2)------设置:t.setUncaughtExceptionHandler(handler)
-------设置默认:调用静态方法 Thread.setDefaultUncaughtExceptionHandler(handler);
-------如果没有为线程设置未捕获异常处理器,则默认为ThreadGroup的未捕获异常处理器,因为ThreadGroup实现了这个接口。
同步
锁的使用方式:
(1)ReentrantLock
特性:锁是可重入的,即如果一个线程得到了某个对象的锁,则可以随便怎么使用锁,即 可以多次使用锁。
(2)ReentrantReadWriteLock
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
Lock readLock=lock.readLock(); 从读写锁中抽取读锁
Lock writeLock=lock.writeLock(); 从读写锁中抽取写锁
条件变量的使用:
条件变量是对于锁功能的补充,比如我们营造一个如下结构的程序:
当多个线程同时访问这个transfer函数,总会有一个串行化顺序,比如Person1得到了这个函数的使用权,但是他现有资金不足以转出,则会停止,等待别人转入等到有足够的钱转出时才继续执行,但是他却没有放弃这个函数的使用权,因此其他人没有办法使用这个函数,因此也没有办法转给Person1钱,因此程序就无法继续。唯一的解决办法就是使用条件变量。
一个锁能有多个条件变量。
试图获得锁:tryLock()
lock.tryLock()如果能获得锁则返回true并获得锁,如果不能则不会阻塞,能够去做其他事。
lock.tryLock(long s,TimeUnit.MILLISECONDS) 试图获得锁,如果不能获得,则阻塞s秒后去做其他事。在阻塞时中断则抛出异常。
Condition c=lock.newCondition();
java中默认每个对象都有一把隐式锁,一把隐式锁只有一个条件变量。因此老式的java程序中使用synchronized关键字进行同步。
老式同步方法:synchronized | 新式同步方法:锁+条件变量 Lock lock=new ReentrantLock();Condition c=lock.newCondition(); | 初始化 |
wait() | c.await() | 放入条件变量的等待队列,并且释放c对应的锁,使得其他人能用这把锁。【阻塞状态】 阻塞时发生中断则抛出异常。 |
wait(long milli) | c.await(long s,TimeUnit.MILLISECONDS) | 相比较上面的方法,如果milli毫秒后会自动变回可运行状态。 |
notifyAll() | c.signalAll() | 把条件变量的等待队列的线程全部放回。【可运行状态】 |
notify() | c.notify() | 随机抽取条件变量等待队列中的一个线程放回变成可运行状态。 |
比较而言:synchronized比较方便。
缺点:以上的同步方法需要我们自己写代码进行同步,不是自动同步。
监视器:一种OO的同步方法,不需要考虑如何加锁。
同步块:
synchronized(obj)
{
critical section
}
即利用对象的锁进行同步。
volatile变量:
对一个变量使用volatile,则会使得虚拟机和编译器知道这个变量会被并发访问。
废弃方法stop和suspend
stop:由于一个线程可以无缘无故的把另一个线程终止,因此废弃。
suspend:当线程A和B,A获得了对象的锁后B调用suspend把A挂起,后来B想要获得对象的锁,但是这个锁在线程A那,会出现死锁。
线程安全数据结构
阻塞队列:BlockingQueue 适合用于生产者-消费者模型
Queue是BlockingQueue的父类:
阻塞队列提供的方法如下:
因此多个阻塞的两个方法。
(1)LinkedBlockingQueue<T>:基于链表,容量无限阻塞队列。
(2)ArrayBlockingQueue<T>:一个由数组支持的有界阻塞队列。有界缓冲区
(3)PriorityBlockingQueue<T>:无界阻塞优先级队列。
(4)DelayQueue<T>:队列中的元素都需要实现Delayed接口和Comparable接口。当元素的延迟用完(小于0)才能从队列中删除。
并发队列ConcurrentLinkedQueue:一个基于链接节点的无界线程安全队列。
1.offer
2.poll
3.peek
并发散列映射表ConcurrentHashMap<K,V>:支持多个读写器
下面是原子性操作:
1.putIfAbsent(key,value):添加
- if (!map.containsKey(key)) return map.put(key, value); else return map.get(key);
- 如果key原来没有,则插入。
- 如果key原来有,则返回旧值。
3.remove(key,value):移除key-value对,如果不在映射中,则不执行任何操作
- if (map.containsKey(key) && map.get(key).equals(value)) {
map.remove(key);
return true;
} else return false;
4.replace(key,oldvalue,newValue):替换
- if (map.containsKey(key) && map.get(key).equals(oldValue)) {
map.put(key, newValue);
return true;
} else return false;
写时复制数组CopyOnWriteArryaList<T>:每个修改他的线程都有这个数组的一份拷贝。
任何collection类通过包装器能够变为线程安全。
Callable<T>:类似Runnable,唯一的区别就是前者有返回值,后者无返回值,因此前者可以用于异步计算。
Future<T>用于保存Callable异步计算的结果。
1.get() 计算完之前阻塞,计算好了则返回结果。
2.isDone()返回是否计算完毕
多线程实现另外几种方法:
1.FutureTask类
结合以上两个接口,FutureTask是实现了以上两个接口的类,能够把Callable作为参数传进去,可以利用FutureTask得到Callable计算的结果。
FutureTask<T> implements Runnable,Future<T>
构造:FutureTask(Callable<T>c);
放入线程:Thread t=new Thread(ft);
开始执行:t.start();
取得结果:ft.get();
2.Executors类 线程池类
线程池的优点:
(1)线程可以重用。
(2)限制线程个数。
线程池的变种:
1.创建线程池
(1)ExecutorService pool=Executors.newCachedThreadPool():在必要的时候能创建线程
(2)ExecutorService pool=Executors.newFixedThreadPool(int n);在线程池中创建固定数量的线程
(3) ExecutorService pool=Executors.newSingleThreadExecutor();线程池中只有一个线程,顺序执行线程。
2.提交作业:
(1)提交一个任务
- Future<T> result=pool.submit(Callable<T>task)
- Future<T>result=pool.submit(Runnable task)
(2)提交多个任务
- ArrayList<Callable<T>>task;
- List<Future<T>>results=pool.invokeAll(task); 提交所有任务。
Future<T>result=pool.invokeAny(task); 任意提交其中一个已完成任务。
3.result.get()可得结果
4.pool.sutdown()在用完线程池后关闭。
预定时间执行线程池:
ScheduledExecutorService pool=Executors.newScheduledThreadPool(int threads);线程池中只有一个线程,顺序执行线程。
pool.schedule(Callable<T>c,long delay,TimeUnit); 在delay秒后启动任务。
pool.scheduleAtFixedRate(Runnable com,long initialDelay,long period,TimeUnit); 周期性的启动任务。
控制线程组:ExecutorCompletionService<T> 内含阻塞队列
构造:ExecutorCompletionService<T> ecs=new ExecutorCompletionService(Executor e);
提交:ecs.submit(Callable<T>c);
取得结果是一个阻塞队列,队列中的元素是Future<T>:ecs.take().get();
一些同步方法:
1.CyclicBarrier 栅栏:顾名思义,就是如果在代码某处设个栅栏,则线程会执行到那停止,等到所有线程都到了,再一起执行。
CyclicBarrier cb=new CyclicBarrier(int n,Runnable action); 规定n个线程,如果n个线程到齐,则执行action。
cb.await();
cb.await(int n,TimeUnit);
2.倒计时门栓CountDownLatch 等到count变为0才开始执行。
CountDownLatch cdl=new CountDownLatch(int count);
cdl.await();等待直到count=0;
countDown();
3.同步队列SynchronousQueue<T>:put后阻塞等待take,take时阻塞等待put
- 例:put(1);后则会阻塞,直到调用take()获取1为止。
- 例:take()后会阻塞直到放入一个元素为止。
4.semaphore:
- 构造:Semaphore(int n); 初始为n的信号量
- acquire() n<=0时阻塞,否则n--;
- release() n++,释放acquire阻塞的线程。
Swing与线程:
Swing中最基本的线程:
- main线程
- 实现分派线程 接收actionPerformed或paintComponent
- gc垃圾回收线程
Swing不是线程安全的。
设置组件属性一定要在组件实现之前。
setVisible(),pack,add称为组件实现。
在Java中,键盘输入、鼠标点击或者应用程序本身产生的请求会被封装成一个事件,放进一个事件队列中,java.awt.EventQueue对象负责从这个队列中取出事件并派发它们。而EventQueue的派发有一个单独的线程管理,这个线程叫做事件派发线程(Event Dispatch Thread),也就是EDT。此外,Swing的绘制请求也是通过EDT来派发的。
EventQueue.invokeLater(Runnable r);把r任务放到事件队列中等待事件分派线程执行。
当需要更新Swing的内容时,则需要将这段代码放入r类中。