多线程与并发:
之前就总提过线程安不安全问题,线程安全就适合多线程,线程不安全就适合单线程,这里我们就来讲一下多线程。
进程与线程
- 任一时刻,CPU总是运行一个进程,其他进程处于非运行状态.进程:是程序的一次执行过程
- 区分进程的条件:所执行的程序和数据集合。
两个进程即使执行在相同的程序上,只要他们运行在不同的数据集合上,他们也是两个进程。例如:多个用户同时调用同一个编译程序编译他们编写的C语言源程序,由于编译程序运行在不同的数据集合(不同的C语言源程序)上,于是产生了一个个不同的进程 - 线程:是进程中的一个执行路径,线程之间可以自由切换,并发执行
- 一个进程可以包括多个线程,每个线程都可以使用这些共享内存
- 一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存
- 一个防止他人进入的简单方法,就是门口加一把锁.先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去.这就叫”互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域.
- 某些内存区域,只能供给固定数目的线程使用
- 并行:就是两个任务同时运行(多个CPU)
并发:是指两个任务同时请求运行,而处理器一次只能接受一个任务,就会把两个任务安排轮流执行,由于CPU时间片运行时间较短,就会感觉两个任务在同时执行
多线程实现的三种方式:
- 继承Thread类(实现了Runnable接口):
将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
然后,下列代码会创建并启动一个线程:
PrimeThread p = new PrimeThread(143);
p.start();
- 实现Runnable接口:
声明实现 Runnable接口的类,该类然后实现run方法。然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动。例如,计算大于某一规定值的质数的线程可以写成:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
然后,下列代码会创建并启动一个线程:
PrimeRun p = new PrimeRun(143);
Thread t=new Thread(p);
t.start();
(1)方法实例化一个线程时,多次调用start()也只能启动一个线程。实例化多个线程实例,每个实例调用一次start()可以启动多个线程,但线程中的资源也是多份的。继承了Tread类后不能同时继承其它类了。
(2)方法可以实例化多个线程,每个实例调用一次start()就可以实现多个线程,并且可以实现多个线程共享同一份资源 实现了Runnable接口后还可实现其它接口和继承其它类。
注:如果现实问题中要求必须创建多个线程来执行同一任务,而且这多个线程之间还将共享同一个资源,那么就可以使用实现Runnable接口的方式来创建多线程程序
3. 实现Callable接口(JAVA 5开始提供)
可以返回结果(通过Future),也可抛出异常
需要实现的是call()方法
public class MultiThread_Test {
public static void main(String[] args) throws Exception {
ExecutorService es = Executors.newSingleThreadExecutor();
// 自动在一个新的线程上启动 MyCallable,执行 call 方法
Future<Integer> f = es.submit(new MyCallable());
// 当前 main 线程阻塞,直至 future 得到值
System.out.println(f.get());
es.shutdown();
}
}
class MyCallable implements Callable<Integer> {
public Integer call() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 123;
}
}
线程休眠(休眠后线程会交替执行)
线程休眠的目的是使线程让出CPU的使用权.当线程休眠时,会将CPU资源的使用交给其他线程,以便能够线程之间的轮换调用.当休眠一定时间后,线程就会苏醒,然后进入准备状态等待执行.
- 不休眠的时候,若线程A到了cpu分配的时间,cpu转向别的进程,然后转回来看A和B谁先抢到
- sleep()定义在Thread.java中。
sleep()的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。(不会释放对象锁)
public class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public synchronized void run() {
try {
for(int i=0; i <10; i++){
System.out.printf("%s: %d\n", this.getName(), i);
// i能被4整除时,休眠100毫秒
if (i%4 == 0)
Thread.sleep(100); //TimeUnit.SECONDS.sleep(sleppTime)
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- wait()的作用也是让当前线程由“运行状态”进入“等待(阻塞)状态”。但是,wait()会释放对象的同步锁,而sleep()则不会释放锁。
join与中断
- join()方法:加入线程,让调用的线程先执行指定时间或执行完毕,再执行线程的后面部分
- Thread.interrupted():测试中断状态,此方法会把中断状态清除
中断方法
- 在主线程里打了一个中断标记,然后在要中断的线程里Sleep的catch里再打一个中断标记,然后在循环里判断下是否中断然后break
- 自定义标记的方式:
public class ThreadDemo2 {
public static void main(String[] args){
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
for(int i=0;i<=30;i++){
System.out.println(Thread.currentThread().getName()+"--"+i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i==20){
mr.flag=false;
}
}
}
}
class MyRunnable implements Runnable{
public boolean flag = true;
public MyRunnable(){
flag = true;
}
@Override
public void run() {
int i = 0;
while(flag){
System.out.println(Thread.currentThread().getName()+"--"+(i++));
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
守护线程与yield
- 线程分为两种:守护线程和用户线程,当进程中没有用户进程时,Java虚拟机将退出
- t.setDaemon(true):把线程设置为守护线程
- yield:暂停当前正在执行的线程对象,并执行其他线程(让一次)
- 优先级高可以提高该线程抢占CPU时间片的概率大
线程同步
- 多线程共享数据:多线程同时处理同一个资源
//共享数据会发生线程不安全的问题
//共享数据必须使用同步(线程锁)//排队轮流
public class ThreadDemo3 {
public static void main(String[] args) {
MyRunnable2 mr = new MyRunnable2();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
class MyRunnable2 implements Runnable{
private int ticket = 10;
@Override
public void run() {
for(int i=0;i<300;i++){
if (ticket>0){
ticket--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("您购买的票剩余"+ticket+"张");
}
}
}
}
- 所谓同步是指多个线程在同一个时间段内只能有一个线程执行指定代码,其他线程要等待此线程完成之后才可以继续执行
- 线程进行同步,有以下三种方法:
- 同步代码块:
synchronized(要同步的对象){
要同步的操作;
}
- 同步代码块:
public void run() {
for(int i=0;i<300;i++){
synchronized (obj) { //同一把锁,括号里也可以是this 代码块结束以后才会释放锁
if (ticket > 0) {
ticket--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("您购买的票剩余" + ticket + "张");
}
}
}
}
- 同步方法(同步对象是当前对象)
public synchronized void method(){
要同步的操作;
} - Lock接口(ReentrantLock):更灵活
//互斥锁
ReentrantLock lock = new ReentrantLock();
private void method(){
lock.lock();//锁
try {
if (ticket > 0) {
ticket--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("您购买的票剩余" + ticket + "张");
}
}finally {
lock.unlock();//释放锁 加finally为了确保它一定会释放,要不然就是死锁
}
}
- 同步准则:
- 线程同步可解决线程安全问题,但是会降低性能,所以使代码块保持简短,把不随线程变化的预处理和后处理移出同步代码块
- 不要阻塞
- 在持有锁的时候,不要对其它对象调用其同步方法
线程死锁
- 过多的同步有可能出现死锁,死锁的操作一般都是在程序运行的时候才可能出现
- 在一个同步方法中调用了另一个对象的同步方法,可能产生死锁
线程池
- 线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中,然后对这些资源进行复用,减少频繁创建和销毁对象
- 线程池的顶级接口是Executor,是一个执行线程的工具,线程池接口是ExecutorService
//创建线程池(4种)
//1 创建一个单线程的线程池
ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(new MyRunable4());//执行完一个任务再执行下一个任务
es.execute(new MyRunable4());
//2 创建固定大小的线程池
ExecutorService es = Executors.newFixedThreadPool(2);//交替执行
es.execute(new MyRunable4());
es.execute(new MyRunable4());
//3 创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需的线程
//那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时
//此线程池又可以智能的添加新线程来处理任务,此线程池不会对线程池大小做限制
//线程池的大小完全依赖于操作系统(JVM)能够创建的最大线程大小
ExecutorService es = Executors.newCachedThreadPool();
//4 创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求
ScheduledExecutorService es = Executors.newScheduledThreadPool(3);
es.schedule(new MyRunable4(),3000,TimeUnit.MILLISECONDS);//3000是延时
es.shutdown();//结束