Java多线程技术
进程与线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。打开任务管理器可以看到当前电脑中正在运行的进程
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行.。
一个进程最少有一个线程。
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
可以看到我的电脑中有259个进程,三千多个线程。(相当于我的电脑中有259个程序,每个程序大于有10多个线程)
线程的6种状态:
NEW(未启动的线程)
RUNNABLE(执行的线程)
BLOCKED(被阻塞的线程)
WAITING(无限期等待的线程)
TIMED_WAITING(有限期等待的线程)
TERMINATED(已结束的线程)
JAVA线程创建的三种方式
1、Thread继承
通过创建一个类来继承Thread类,在这个类中重写run()方法,通过这个类创建的对象就是一个线程。调用start()方法来启动线程而不是调用run方法。
public class MyThread extends Thread{
@Override
public void run() {
//这里的代码就是一条执行的新的路径
//重写此方法实现自己要实现的内容
super.run();
}
}
public static void main(String[] args) {
MyThread my = new MyThread();
my.start();//调用start方法启动线程,而不是调用run方法。
}
2、实现Runnable
通过创建一个类来实现Runnable接口,在这个类中重写run()方法,通过这个类创建的对象是一个线程任务,我们将这个任务传给一个Thread对象即可执行这个线程任务。
public class MyRunnable implements Runnable {
@Override
public void run() {
}
}
public static void main(String[] args) {
//创建一个任务对象
MyRunnable run = new MyRunnable();
//创建一个线程,为其分配一个任务
Thread t = new Thread(run);
//执行这个线程
t.start();
}
实现Runnable与继承Thread相比有如下优势
1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
2,可以避免单继承所带来的局限性
3,任务与线程是分离的,提高了程序的健壮性
4,线程池技术,接受Runnable类型的任务,不接受Thread类型的线程
3、Callable接口(很少用的方式)
创建方式与Runnable相似。创建此类线程会产生一个返回值,如果主程序要获取这个返回值,则主程序会在Callable线程运行结束后再运行。也可以定义等待的时间,如果超过这个时间还没有获得返回值,那么不再等待。
public static void main(String[] args) {
Callable<Integer> c = new MyCallable();
//FutureTask 是一个任务对象,将Callable对象传入这样才能执行
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
}
//该类实现是要传入一个泛型
static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return null;
}
}
3、Thread类常用方法
1.Thread(Runnable target) //(构造方法)传入一个Runnable任务
2.Thread(String name) //(构造方法)为线程取一个名字
3.String getName() //获取该线程的名字
4.void setName(String name) //设置该线程的名字
5.void start() //线程开始
6.static Thread currentThread() //获取当前线程对象
7.static void sleep(long millis) //让当前线程休眠,进入阻塞状态,传入的参数单位为毫秒
8.void setDaemon(boolean on) //将线程设置为守护线程或者用户线程
9.void interrupt() //中断此线程
10.int getPriority() //返回此线程的优先级
11.void setPriority(int newPriority) //更改此线程的优先级
stop()和interrupt()
线程在运行的过程中两种中断线程的方法,一个是stop()方法,一个是interrupt()方法。
Sun公司在JDK1.2版本的时候将stop()方法改为过时了,因为这种中断线程的方式是在外部强制的,就像把一个线程给杀死了。这可能会导致在中断过程数据丢失,所以是不安全的。
使用interrupt()方法则是一种安全的方式,当在线程外部调用interrupt()方法时,JVM会给运行的线程传递一个异常对象,当线程接收到异常对象时,线程就会终止。
intterrupt()方法其实是给该线程打一个中断标记。当线程收到这个标记是,该线程会抛出InterruptedException的异常,进入到catch块。我们可以自定义线程是否死亡。
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
t.interrupt();//让改线程停止
class MyRunnable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("发现了中断标记,但我就不死亡");
}
}
}
}
守护线程和用户线程
用户线程:当一个进程不包含任何的存活的用户线程时,进行结束
守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
使用setDaemon()方法可以将一个线程设置为守护线程或者用户线程(参数为TRUE时,是守护线程,参数为FALSE时为用户线程),一个线程被创建时默认是用户线程。
线程安全
同步:排队执行 , 效率低但是安全. 比如,一群人排着队去吃一锅饭,虽然比较慢,但是不会因为抢着吃一块肉而打架。
异步:同时执行 , 效率高但是数据不安全.比如,一群人同时吃一锅饭,可能就会因为抢一块肉而打起来。
比如买票案例:
三个进程同时卖10张票。
public static void main(String[] args) {
//线程不安全
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (count>0){
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("卖票结束,余票:"+count);
}
}
}
最后的运行结果出现了负数,这显然是不合理的。可以看到我的while判断是大于0才可以进入。但为什么最后会出现-1、-2呢?其实因为有三个进程在执行,当一个进程在count等于1时进入到while语句,但它又刚好没执行到count- -这条语句时,其他俩个进程刚好进来了,这就造成了这种情况。这就是线程不安全。
实现线程安全的方法
如何让线程安全呢,其实线程不安全的原因就是所有线程抢着做一件事。我们可以让所有线程排队去做,就可以实现线程安全,让线程同步的方法有三种。
1、同步代码块
格式:synchronized(锁对象){
要同步的代码
}
锁对象可以是任何对象,但是所有线程必须用一把锁。
实现购票安全:
public static void main(String[] args) {
//线程安全
Runnable run = new Ticket();
//Ticket只new了一次所有其是一把锁
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
synchronized (this) {//将自身(Ticket)对象作为锁
while (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("卖票结束,余票:" + count);
}
}
}
}
这样一个线程进入锁代码块就会上锁,这样其他线程就进入不到代码块中。
2、同步方法
就是通过synchronized关键字来给方法上锁,线程就会同步调用该方法。
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案2 同步方法
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
//当前加锁的对象默认为当前对象this
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;
}
}
3、显示锁Lock
其实无论是同步方法还是同步代码块,我们都称其为隐示锁,只需要我们加上synchronized关键字就可以了,不需要我们关注何时上锁何时开锁。而使用Lock方法需要创建Lock对象,并在需要加锁是手动加锁,在需要解锁时手动解锁。
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案1 显示锁 Lock 子类 ReentrantLock
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
@Override
public void run() {
while (true) {
l.lock();//上锁
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
l.unlock();//开锁
}
}
}
显式锁和隐式锁的区别
(1)synchronized:Java中的关键字,是由JVM来维护的。是JVM层面的锁。
(2)Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。
(3)在使用synchronized关键字的时候,我们使用者根本不用写其他的代码,然后程序就能够获取和释放锁了。那是因为当synchronized代码块执行完成之后,系统会自动的让程序释放占用的锁。synchronized是由系统维护的,如果非逻辑问题的话话,是不会出现死锁的。
(4)在使用lock的时候,我们使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:lock.lock()。释放锁:unlock方法。需要配合tyr/finaly语句块来完成。
(5)synchronized是不可中断的。除非抛出异常或者正常运行完成
(6)Lock可以中断的。
(7)synchronized:非公平锁
(8)lock:两者都可以的。默认是非公平锁。ReentrantLock(boolean fair),true是公平锁,false是不公平锁(上锁之后,其他线程按照先来后到在开锁之后进入就是公平锁。不安先来后到,谁抢到时间片就是谁的为不公平锁)
死锁
线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。
比如:A和B两人分别进入试衣间1和试衣间2,A想等B出来后去试衣间2,自己则在试衣间1中等待,B想等A出来后去试衣间1,自己则在试衣间2中等待,最终2个人都在等对方出来,但是对方都不出来,导致一直僵持。
生产者与消费者问题
当厨师在做菜时,服务员休息状态,当厨师做完菜后,厨师休息,服务员端菜出去,等服务员端空盘子回来后,服务员继续休息,叫厨师继续做菜。依次循环
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndSaste("老干妈小米粥","香辣味");
}else{
f.setNameAndSaste("煎饼果子","甜辣味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;
//true 表示可以生产
private boolean flag = true;
public synchronized void setNameAndSaste(String name,String taste){
if(flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag) {
System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程池
从字面理解,就是线程的池子,线程的容器。
线程在执行任务是它经历的流程是
1、创建线程
2、创建任务
3、执行任务
4、关闭线程
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间.
线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
1、缓存线程池
长度无限制,任务线程传入时自动分配线程,线程不够时自动创建新线程
//创建线程池
ExecutorService service = Executors.newCachedThreadPool();
//指挥线程池执行新的任务
service.execute(new Runnable() {
@Override
public void run() {
}
});
定长线程池
指定线程池线程的个数,任务线程传入时自动分配线程,线程不够时剩余任务线程排队等待线程池中的线程执行完毕
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
}
});
单线程线程池
线程池中只有一个线程,任务线程传入时自动分配线程,一个任务执行时剩余任务线程排队等待线程池中的线程执行完毕
Executors.newSingleThreadExecutor();
周期定长线程池
指定线程池线程的个数,任务线程传入时自动分配线程,可以设定任务线程第一次执行的延时时间和之后每次执行的间隔时间
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
service.schedule(new Runnable() {//定时执行1次
@Override
public void run() {
}
},5,TimeUnit.SECONDS);//5秒钟后执行
service.scheduleAtFixedRate(new Runnable() {//周期执行
@Override
public void run() {
}
},5,1,TimeUnit.SECONDS);//每5秒执行一次