简述:
认识多线程,先了解 程序(program)、进程(process)、线程(thread)。
程序(program):还未执行的静态代码
进程(process):程序的一次执行过程,或正在运行的一个程序,并且有生命周期
线程(thread):进程可以细化为线程,程序内部的一条执行路径,如果一个进程同一时间并行多个线程,就是支持多线程。
Ps:一个进程中的多个线程可以共享堆和方法区,但是栈和程序计数器每个线程都会有。
并行:多个CPU同时执行多个任务,例如:多个人同时做不同的事
并发:一个CPU同时执行多个任务(其实是假的同时执行,采用时间片轮转法,来模拟看似是同时执行多个任务),例如:多个人同时做一件事、秒杀
多线程的创建:
方式一:
1、创建一个继承Thread类的子类
2、重写Thread类的run()方法,此线程执行的操作声明在run()方法中
3、创建Thread类的子类对象
4、通过此对象调用start()方法
ps:
start()方法的作用:启动当前线程或调用当前线程的run()方法
如下demo所示
package cn.latte.thread;public class Test { public static void main(String[] args){ MyThread t = new MyThread(); //给当前线程重命名,除了以下方法,还可以给线程类写有参构造器来重名名 t.setName("线程一"); t.start(); //用匿名内部类的方式调用 new Thread(){ @Override public void run() { for(int i=0;i<100;i++){ if(i%2!=0) System.out.println(Thread.currentThread().getName()+"线程输出奇数:"+i); } } }.start(); //给当前main线程重命名 Thread.currentThread().setName("主线程"); for(int i=0;i<100;i++){ if(i%3==0&&i!=0) System.out.println("main主线程输出3的倍数:"+i); } }}class MyThread extends Thread{ @Override public void run() { for(int i=0;i<100;i++){ if(i%2==0) System.out.println(Thread.currentThread().getName()+"线程输出偶数:"+i); } }}
Thread常用的方法:
yield():释放当前CPU的执行权,进入与其他线程公平争夺CPU使用权的状态
join():也是释放当前的CPU执行权,但是与yield()的差别在:当前线程会让调用join()方法的线程完全执行完毕后(当前线程进入阻塞状态),再进入与其他线程公平争夺CPU使用权的状态
sleep(long millitime):让当前线程睡眠指定的millitime毫秒(进入阻塞状态),时间一到就进入与其他线程公平争夺CPU使用权的状态
isAlive():判断线程是否存活
线程的优先级:
默认优先级5,最高10,最低1
getPriority():获取线程优先级系数
setPriority(int p):设置线程优先级(在调用start()方法之前)
系数越高抢占cpu的概率就越大,并不决定执行的先后顺序
方式二:
1、创建一个实现Runnable接口的类
2、实现类去实现Runnable中的run()
3、创建实现类对象,并将该对象作为参数传递到Thread类的构造器中,创建Thread对象
4、通过Thread类的对象调用start()
如下demo:
package cn.latte.thread;public class TestThread1{ public static void main(String[] args){ RunThread r=new RunThread(); //创建多个线程时,共用一个实现类实例 new Thread(r).start();//调用了Runnable类型的target中的run()方法 new Thread(r).start(); }} class RunThread implements Runnable{ @Override public void run() { for(int i=0;i<100;i++){ if(i%2==0) //此时getName()必须由Thread来调用,因为此时继承的是Object类 System.out.println(Thread.currentThread().getName()+"线程输出偶数:"+i); } } }
方法一和方法二的注意点:
开发中尽量选择方法二,原因:
1、方式一需要实现Thread类,影响了可扩展性,不能再继承其他类,而实现Runnable接口的方式避免了这一点
2、实现更适合处理创建出来的多个线程共享数据的情况。
方式三:
创建过程如下代码:
//1创建一个实现Callable的类class ThreadDemo implements Callable<Integer> { @Override //2、重写Callable中的call()方法 public Integer call() throws Exception { int sum =0; for(int i=0;i<100;i++){ if(i%2==0) { System.out.println(Thread.currentThread().getName() + "线程输出偶数:" + i); sum += i; } } return sum; }}public class CallableCreate { public static void main(String[] args){ //3、创建Callable实现类对象 ThreadDemo t = new ThreadDemo(); //4、此对象传递到FutureTask构造器中,创建FutureTask的对象 FutureTask futureTask=new FutureTask(t); //5、将FutureTask的对象作为参数传递到Thread类中的构造器,创建Thread对象,并调用start()方法 new Thread(futureTask).start(); try { //get()返回值是FutureTask构造器参数Callable实现类重写的call()方法的返回值 Integer o = futureTask.get(); System.out.println("偶数的总和为:"+o); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }}
ps:实现Callable的创建方式比Runnable更强大
1、call()方法可以有返回值
2、call()方法可以抛出异常,被外面的操作捕获,获取异常信息
3、Callable是支持泛型的
方式四:线程池
使用线程池的优点:
1、提高响应速度(减少了创建新线程的时间)
2、降低资源消耗(重复利用线程池中的线程,不用每次都创建)
3、便于线程管理
几个系数:
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有执行任务时最多保持多长时间会终止
具体操作见下面代码:
public class ThreadPool { public static void main(String[] args){ //1、提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1= (ThreadPoolExecutor) service; //设置线程池的属性 //service1.setKeepAliveTime(10000,MILLISECONDS); //service1.setCorePoolSize(15); //2、执行指定线程的操作,提供一个实现Runnable接口或Callable接口的实现类对象 service.execute(new TestRannable()); //3、关闭连接池 service.shutdown(); }}class TestRannable implements Runnable{ @Override public void run() { for(int i=0;i<100;i++){ if(i%2==0) System.out.println(Thread.currentThread().getName()+"线程输出偶数:"+i); } }}
线程的状态:
如下图,详细描述了线程各个状态之间是如何转换的
线程的安全问题:
先举一个经典的买票问题:
public class TestWindow{ public static void main(String[] args){ Window w = new Window(); //总共100张票两个窗口卖 new Thread(w).start(); new Thread(w).start(); }}class Window implements Runnable { private int ticket=100; @Override public void run() { while(true){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"票号为:"+ticket); }else{ break; } ticket--; } }}
1、首先引出会出现的问题:卖票过程中会出现重票、错票等现象;两个窗口都卖出100号票,有窗口卖出了0号票
2、问题出现的原因:某个线程正在运行run()方法,且未完成时,其他线程也进入run()方法
3、解决方法:当一个线程在操作ticket时,其他线程不能参与进来,直到该线程操作完后,其他线程才开始操作ticket。这种情况即使该线程出现阻塞状态,也不能被改变。
4、java中,通过同步机制解决这种问题
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
public class TestWindow{ public static void main(String[] args){ Window w = new Window(); new Thread(w).start(); new Thread(w).start(); }}class Window implements Runnable { private int ticket=100; Object obj = new Object(); @Override public void run() { while (true) { //这个对象其实可以直接调当前对象this synchronized(obj) { if(ticket > 0){ System.out.println(Thread.currentThread().getName() + "票号为:" + ticket); }else{ break; } ticket--; } } }}
ps:
1、操作共享数据的代码,即为需要被同步的代码
2、同步监视器:俗称"锁",任何一个对象都可以充当锁
要求:多个线程必须要共用同一把锁(也就是同一个对象)
3、上述代码是实现Rannable方式的同步。那么继承Thread类的方式的同步就要注意了:还是以卖票为例,如下代码
package cn.latte.thread;public class TestWindow{ public static void main(String[] args){ new Window().start(); new Window().start(); }}class Window extends Thread { //因为创建多个线程需要创建多个Window对象,所以这里的ticket变量需要是静态的 static int ticket=100; //由于锁必须用同一个对象,所以这里加了静态修饰 static Object obj = new Object(); @Override public void run() { while (true) { //这里的同步监视器不能使用this(当前对象),因为在启动线程时,可能会创建多个对象,慎用 //但是这里还有另一种写法:synchronized(Window.class) synchronized(Window.class) { if(ticket > 0) { System.out.println(Thread.currentThread().getName() + "票号为:" + ticket); }else{ break; } ticket--; } } }}
4、同步代码块的范围一定要把握好,不能多也不能少。
方法二:同步方法
如果一个同步代码块刚好等于一个方法的范围,那么就可以在该方法前加synchronized等关键字,此方法变为同步方法。
分为两种情况:
实现Rannable方式:只需在方法前加synchronized关键字,此时的同步监视器就是this
继承Thread方式:需要在方法前加synchronized和static关键字,此时的同步监视器就不是this了,是继承于Thread子类的类对象 "子类名.class"
注意:同步的方式解决了线程安全的问题,但同时在操作代码时,只能有一个线程参与,其他线程等待,相当于单线程,相率低。
方法三:ReentrantLock(JDK5.0新增)
具体使用方法如下代码
public class LockTest{ public static void main(String[] args){ Window1 w = new Window1(); //总共100张票两个窗口卖 new Thread(w).start(); new Thread(w).start(); }}class Window1 implements Runnable { private int ticket=100; //1、实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while(true){ try { //2、调用锁定方法 lock.lock(); if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "票号为:" + ticket); } else { break; } ticket--; }finally { //3、调用解锁方法 lock.unlock(); } } }}
它与synchronized的区别在于,synchronized机制在执行完同步代码后会自动的释放同步监视器,而lock需要手动的启动和结束同步,相对来说lock更灵活
线程的死锁问题:
简述:不同的线程分别占用着对方的同步资源不放手,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
解决的方法:
1、尽量少使用同步锁,能不使用就不使用
2、避免同步的嵌套
线程通讯:
还是用上面的例子引出,现在想依次输出票号,比如线程一100号、线程二99号、线程一98号......依次输出,该怎么解决呢?
上代码:
public class TestWindow{ public static void main(String[] args){ Window w = new Window(); new Thread(w).start(); new Thread(w).start(); }}class Window implements Runnable { private int ticket=100; @Override public void run() { while (true) { synchronized(this) { //线程一进入notify无反应,紧接着下面的线程一被wait,但是锁释放 //所以在线程一被阻塞时,线程二也可以进入同步代码块,执行notify,释放了线程一的阻塞状态 //以此循环,就达到了依次输出票号的目的 notify(); if(ticket > 0){ System.out.println(Thread.currentThread().getName() + "票号为:" + ticket); }else{ break; } ticket--; try { //线程一进入,被wait,进入阻塞状态,但是锁是释放的 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }}
其中的提到了两个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放锁
notify():一旦执行此方法,就会唤醒一个被wait的线程,如果有多个线程被wait,那就唤醒优先级高的那个线程
notifyAll():从字面意思也不难理解,会唤醒所有被wait的线程
注意:以上的三个方法必须使用在同步代码块或同步方法中;并且这三个方法的调用者必须是同步代码块或同步方法中的同步监视器(锁),否则会出现异常,上面代码因为同步锁是this,所以调用者忽略;这三个方法定义在Object类中
wait()和sleep()两个方法都会使线程阻塞,不同点是:
1、sleep是Thread类中的方法,但是wait是Object中的方法
2、wait只能在同步中使用,而sleep不受限
3、如果两个方法都在同步中使用,sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。