为什么要使用多线程
网站的访问量非常的巨大,常常会出现多个用户端访问客户端的情况,多线程可以使我们更好去应对这种高并发的情况,最大可能去充分利用cpu,加快访问速度,提升用户使用的舒适度
分清进程和线程
进程:
1. 进程是程序的一次动态执行过程, 占用特定的地址空间。
2. 每个进程由3部分组成:cpu、data、code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,这样的缺点是:浪费内存,cpu的负担较重。
3. 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占CPU的使用权。
4. 进程的查看
Windows系统: Ctrl+Alt+Del,启动任务管理器即可查看所有进程。
Unix系统: ps or top。
线程:
一个进程可以产生多个线程。同多个进程可以共享操作系统的某些资源一样,同一进程的多个线程也可以共享此进程的某些资源(比如:代码、数据),所以线程又被称为轻量级进程(lightweight process)。
1. 一个进程内部的一个执行单元,它是程序中的一个单一的顺序控制流程。
2. 一个进程可拥有多个并行的(concurrent)线程。
3. 一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。
4. 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
5. 线程的启动、中断、消亡,消耗的资源非常少。
线程和进程的区别:
1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销
2. 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
3.线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。
4. 多进程: 在操作系统中能同时运行多个任务(程序)。
5. 多线程: 在同一应用程序中有多个顺序流同时执行。
6. 线程是进程的一部分,所以线程有的时候被称为轻量级进程。
7. . 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
8. 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
进程与程序的区别:
程序是一组指令的集合,它是静态的实体,没有执行的含义。而进程是一个动态的实体,有自己的生命周期。一般说来,一个进程肯定与一个程序相对应,并且只有一个,但是一个程序可以有多个进程,或者一个进程都没有。除此之外,进程还有并发性和交往性。简单地说,进程是程序的一部分,程序运行的时候会产生进程。
创建多线程的四大方式
一.继承Thread类(这种不推荐,类单继承,后期不易于代码维护)
/*** 创建线程的方式一:* 创建:继承Thread+重写run方法* 启动:.start()* 不推荐,因为java中类单继承,不易于后期代码维护* @author Zrd*/public class StartThread extends Thread{/*** run方法是线程入口点*/@Overridepublic void run () {for(int i=0;i<20;i++){System.out.println("听歌");}}public static void main(String[] args){new StartThread().start();for(int i=0;i<100;i++){System.out.println("敲代码");}}}
二.实现Runnable接口(推荐,避免单继承的局限性)
/*** 创建线程的方式二:* 创建:实现Runnable+重写run* 启动:创建实现类对象+Thread对象+start* 推荐:避免了单继承的局限性,优先使用接口,便于后期代码维护* 方便资源共享* @author Zrd*/public class StartRunnable implements Runnable{/*** run方法不能抛出异常,只能try catch*/@Overridepublic void run () {for(int i=0;i<20;i++){System.out.println("听歌");}}public static void main (String[] args) {//创建代理对象new Thread(new StartRunnable()).start();for(int i=0;i<100;i++){System.out.println("敲代码");}}}
三.实现Callable接口(推荐,juc中使用,属于高级并发编程,笔者暂时能力有限,先临摹,等深入了解后再来做分析)
/*** 创建线程的方式三:* Callable+重写run;创建执行服务;提交执行;获取结果;关闭服务* 高级并发编程中使用* @author Zrd*/public class StartCallable implements Callable<Boolean> {@Overridepublic Boolean call () throws Exception {for(int i=0;i<20;i++){System.out.println("听歌"+i);}return true;}public static void main (String[] args) throws Exception{StartCallable c1 = new StartCallable();StartCallable c2 = new StartCallable();StartCallable c3 = new StartCallable();//创建执行服务ExecutorService executorService = Executors.newFixedThreadPool(3);//提交执行Future< Boolean > s1 = executorService.submit(c1);Future< Boolean > s2 = executorService.submit(c2);Future< Boolean > s3 = executorService.submit(c3);//获取结果s1.get();s2.get();s3.get();//关闭服务executorService.shutdownNow();}}
四:线程池(笔者暂时还未涉略,后期深入理解后再做补充)
线程状态
一个线程对象在它的生命周期内,需要经历5个状态:
操控线程状态的几种方式:
一.sleep(睡眠)
使用sleep,让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。(不会释放锁)
public class TestThreadState {public static void main(String[] args) {StateThread thread1 = new StateThread();thread1.start();StateThread thread2 = new StateThread();thread2.start();}}//使用继承方式实现多线程class StateThread extends Thread {public void run() {for (int i = 0; i < 100; i++) {System.out.println(this.getName() + ":" + i);try {Thread.sleep(2000);//调用线程的sleep()方法;} catch (InterruptedException e) {e.printStackTrace();}}}}
二:yield (礼让)
使用yield,让正在运行的线程直接进入就绪状态,让出CPU的使用权。(线程重新竞争cpu的使用权,所以礼让不一定成功)
public class TestThreadState {public static void main(String[] args) {StateThread thread1 = new StateThread();thread1.start();StateThread thread2 = new StateThread();thread2.start();}}//使用继承方式实现多线程class StateThread extends Thread {public void run() {for (int i = 0; i < 100; i++) {System.out.println(this.getName() + ":" + i);Thread.yield();//调用线程的yield()方法;}}}
三.线程的联合join()
线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。如下面示例中,“爸爸线程”要抽烟,于是联合了“儿子线程”去买烟,必须等待“儿子线程”买烟完毕,“爸爸线程”才能继续抽烟。
public class TestThreadState {public static void main(String[] args) {System.out.println("爸爸和儿子买烟故事");Thread father = new Thread(new FatherThread());father.start();}}class FatherThread implements Runnable {public void run() {System.out.println("爸爸想抽烟,发现烟抽完了");System.out.println("爸爸让儿子去买包红塔山");Thread son = new Thread(new SonThread());son.start();System.out.println("爸爸等儿子买烟回来");try {son.join();} catch (InterruptedException e) {e.printStackTrace();System.out.println("爸爸出门去找儿子跑哪去了");// 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束System.exit(1);}System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子");}}class SonThread implements Runnable {public void run() {System.out.println("儿子出门去买烟");System.out.println("儿子买烟需要10分钟");try {for (int i = 1; i <= 10; i++) {System.out.println("第" + i + "分钟");Thread.sleep(1000);}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("儿子买烟回来了");}}
获取线程基本信息的方法
线程的常用方法一
public class TestThread {public static void main(String[] argc) throws Exception {Runnable r = new MyThread();Thread t = new Thread(r, "Name test");//定义线程对象,并传入参数;t.start();//启动线程;System.out.println("name is: " + t.getName());//输出线程名称;Thread.currentThread().sleep(5000);//线程暂停5分钟;System.out.println(t.isAlive());//判断线程还在运行吗?System.out.println("over!");}}class MyThread implements Runnable {//线程体;public void run() {for (int i = 0; i < 10; i++)System.out.println(i);}}
线程的优先级
一.处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。
二.线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。
三.使用下列方法获得或设置线程对象的优先级
int getPriority();
void setPriority(int newPriority);
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。
public class TestThread {public static void main(String[] args) {Thread t1 = new Thread(new MyThread(), "t1");Thread t2 = new Thread(new MyThread(), "t2");t1.setPriority(1);t2.setPriority(10);t1.start();t2.start();}}class MyThread extends Thread {public void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + ": " + i);}}}
线程同步
一.什么是线程同步?
线程同步的本质是一种等待机制,在处理多线程问题中,多个线程同时访问一个对象,有些甚至想修改这个对象,假如我们不做人很处理,就会出现数据不正确(线程不安全)的问题,解决思路:多个需要同时访问这个对象的线程进入这个对象的等待池形成队列,等前面的线程处理完毕后,下一个线程再使用
二. 实现线程同步
使用synchronized关键字;它包括两种用法:synchronized 方法和synchronized 块。
1.synchronized 方法
通过在方法声明中加入 synchronized关键字来声明,语法如下:
public synchronized void accessVal(int newVal);
synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
2.synchronized块
相比起synchronized 方法的优势:精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。
synchronized(syncObject){//允许访问控制的代码 }
自定义一个类测试synchronized 方法,代码如下
/*** 线程安全:在并发的时候保证数据的正确性,效率尽可能的高* 1。采用synchronized 同步方法 (锁的是this)* @author Zrd*/public class SynchronizeTest01 {public static void main (String[] args) {SafeWeb12306 safeWeb12306 = new SafeWeb12306();new Thread(safeWeb12306,"线程一").start();new Thread(safeWeb12306,"线程二").start();}}/*** 定义一个抢票类,体会同步方法*/class SafeWeb12306 implements Runnable{/*** 票数*/private int votes=10;/*** 标识 判断是否还有票*/private boolean flag=true;@Overridepublic void run () {while (flag){test();}}public synchronized void test(){/*** 当票数<=0 时候 直接跳出方法*/if(votes<=0){flag=false;return;}//模拟延时try {Thread.sleep(200);}catch (InterruptedException e){//阻塞异常e.printStackTrace();}System.out.println(Thread.currentThread().getName()+votes--);}}
自定义一个synchronized块案例,代码如下:
/*** 线程安全* 2.采用synchronized(obj){} 同步块直接锁资源,提高性能* @author Zrd*/public class SynchronizeTest02 {public static void main (String[] args) {Account account = new Account(150, "总账户");Withdrawal w1 = new Withdrawal(account, "一号操作员", 30);Withdrawal w2 = new Withdrawal(account, "二号操作员", 30);Withdrawal w3 = new Withdrawal(account, "三号操作员", 90);Withdrawal w4 = new Withdrawal(account, "四号操作员", 90);new Thread(w1).start();new Thread(w2).start();new Thread(w3).start();new Thread(w4).start();}}/*** 存款账户类*/class Account{/*** 余额*/int balance;/*** 账户姓名*/String accountName;public Account () {}public Account (int balance, String accountName) {this.balance = balance;this.accountName = accountName;}}/*** 模拟取款类*/class Withdrawal implements Runnable{/*** 账户对象*/private Account account;/*** 操作员*/private String operator;/*** 取走的金额*/private int fundsWithdrawn;public Withdrawal (Account account, String operator, int fundsWithdrawn) {this.account = account;this.operator = operator;this.fundsWithdrawn = fundsWithdrawn;}public Withdrawal () {}/*** 使用synchronized块锁账户资源 account*/@Overridepublic void run () {//采取优化,当余额<0直接跳出方法, 不需要去锁资源 ,提高性能if(account.balance-fundsWithdrawn<0){return;}/*** 注意synchronized只能锁住一个对象* 当需要同时锁多个对象,将对象进行包装,锁住包装类*/synchronized (account){if(account.balance-fundsWithdrawn<0){return;}else {System.out.println(operator+"取走了"+account.accountName+" "+fundsWithdrawn+"元,剩下"+(account.balance-fundsWithdrawn));//总账户的钱减去取走的钱account.balance=account.balance-fundsWithdrawn;}}}}
死锁的出现和解决方案
一.什么时候会出现死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。
某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
死锁的解决方案
出现死锁一般是锁中嵌套锁,解决的方案是:将锁从另外一个锁中解套出来
下面编写一个案例体会死锁的出现,和解决方案
/*** 死锁:过多的同步可能造成相互不释放资源* 从而相互等待,一般发生与同步块中持有多个对象的锁* 解决方案:不要在锁中嵌套锁,解套* @author Zrd*/public class DeadLock {public static void main (String[] args) {for(int i=0;i<100;i++){MakeUp g1 = new MakeUp(true, "一号女孩");MakeUp g2 = new MakeUp(false, "二号女孩");new Thread(g1).start();new Thread(g2).start();}}}/*** 口红类*/class Lipstick{}/*** 镜子类*/class Mirror{}/*** 化妆*/class MakeUp implements Runnable{/*** 口红对象,采用static 保证只有一只口红* 对static对象初始化*/static Lipstick lipstick=new Lipstick();/*** 镜子对象,采用static 保证只有一面镜子*/static Mirror mirror=new Mirror();/*** true->选择口红,false->选择镜子*/boolean choice;/*** 化妆女孩对象*/String girl;public MakeUp () {}public MakeUp (boolean choice, String girl) {this.choice = choice;this.girl = girl;}/***包含两个案例一个会出现死锁,一个是基于对死锁的解决*/@Overridepublic void run () {/*** 死锁案例*/try {makeUpDeadLock();} catch (InterruptedException e) {e.printStackTrace();}/*** 解决后的案例*/try {makeUpDeadLockSolved();} catch (InterruptedException e) {e.printStackTrace();}}/*** 死锁案例* 化妆过程 锁中嵌套了锁,容易出现死锁*/private void makeUpDeadLock() throws InterruptedException {if (choice){//获得口红锁synchronized (lipstick){System.out.println(this.girl+"获得口红");//1秒后 ,获取镜子Thread.sleep(1000);//获得镜子锁synchronized (mirror){System.out.println(this.girl+"获得镜子");}}}else {//获得镜子锁synchronized (mirror){System.out.println(this.girl+"获得镜子");//1秒后 ,获取口红Thread.sleep(1200);//获得口红锁synchronized (lipstick){System.out.println(this.girl+"获得口红");}}}}/*** 解决死锁案例* 思路,解套*/private void makeUpDeadLockSolved() throws InterruptedException {if (choice){//获得口红锁synchronized (lipstick){System.out.println(this.girl+"获得口红");}//1秒后 ,获取镜子Thread.sleep(1000);//获得镜子锁synchronized (mirror){System.out.println(this.girl+"获得镜子");}}else {//获得镜子锁synchronized (mirror){System.out.println(this.girl+"获得镜子");}//1秒后 ,获取口红Thread.sleep(1000);//获得口红锁synchronized (lipstick){System.out.println(this.girl+"获得口红");}}}}
解决并发的几种模式(笔者还没开始学juc 暂时写一种 后续学到了再做补充)
一 生产者于消费者模式
两种方式实现
1.管程法:通过定义一个缓冲容器,判断缓冲容器中空间存放情况,有空间生产者就生产数据,有数据消费者就可以进行消费,不能的话就线程等待Thread.wait(),生产和消费之间可以相互通知Thread.notiflyAll()
下面编写一个案例体会管程法:
package 多线程.并发.几种解决并发的模式;/*** 生产者与消费者实现方式一:管程法* 解决的问题:1.什么时候能生产,2什么时候能消费* 借助缓冲容器* @author Zrd*/public class ManagementMethod {public static void main (String[] args) {BufferContainer bufferContainer = new BufferContainer();new Thread(new Producer(bufferContainer)).start();new Thread(new Consumer(bufferContainer)).start();}}/*** 生产者*/class Producer implements Runnable{BufferContainer bufferContainer;public Producer (BufferContainer bufferContainer) {this.bufferContainer = bufferContainer;}@Overridepublic void run () {//生产数据for(int i=0;i<100;i++){bufferContainer.push(new Data(i));System.out.println("生产了"+i+"数据");}}}/*** 消费者*/class Consumer implements Runnable{BufferContainer bufferContainer;public Consumer (BufferContainer bufferContainer) {this.bufferContainer = bufferContainer;}@Overridepublic void run () {//消费数据for(int i=0;i<20;i++){bufferContainer.pop();System.out.println("消费了"+i+"数据");}}}/*** 数据缓冲区* 本质上是定义一个缓冲容器*/class BufferContainer{/*** 定义一个大小为10的Data数组,用作数据缓存容器*/Data[] datas=new Data[10];/*** 计数器*/int count=0;/*** 存数据*/public synchronized void push(Data data) {/*** 什么时候能生产数据,容器中存在空间* 当容器中不存在空间,只能等待*/if(count==datas.length){//this.wait() 当前线程处于阻塞状态,等待消费者的通知try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//容器有空间可以消费datas[count]=data;count++;//通知消费者消费this.notifyAll();}/***获取数据*/public synchronized Data pop() {/*** 何时能消费?* 判断容器中是否存在数据* 容器中没有数据,只能等待*/if(count==0){//this.wait() 当前线程处于阻塞状态,等待生产者的通知try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//存在数据可以消费count--;//通知生产者生产this.notifyAll();return datas[count];}}/*** 数据*/class Data{/*** 加入一个成员 好区别*/int i;public Data (int i) {this.i = i;}public Data () {}}
2.信号灯法:通过定义一个标识位*,控制生产和消费的实现
下面编写一个案例体会信号灯法:
package 多线程.并发.几种解决并发的模式;import java.lang.reflect.TypeVariable;/*** 生产者与消费者实现方式二:信号灯法* 解决的问题:1.什么时候能生产,2什么时候能消费* 借助标识位* @author Zrd*/public class SemaphoreMethod {public static void main (String[] args) {Tv tv = new Tv();new Thread(new Actor(tv)).start();new Thread(new Audience(tv)).start();}}/*** 电视类* 演员表演 和 观众观看*/class Tv{/*** 选择做的事情*/String voice;/*** true->演员表演,false->观众观看*/boolean flag;/*** 演员表演(生产)*/public synchronized void play(){/*** 借助标识位,控制能否表演* 等待*/if(!flag){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//开始表演(生产)System.out.println(this.voice);//改变标识位flag=false;//通知观看(消费) 唤醒等待的线程this.notifyAll();}/*** 观众观看(消费)*/public synchronized void watch(){/*** 借助标识位,控制能否观看* 等待*/if(flag){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//开始观看(消费)System.out.println(this.voice);//改变标识位flag=true;//通知生产(生产) 唤醒等待的线程this.notifyAll();}}class Actor implements Runnable{/*** 操作tv对象*/Tv tv;public Actor (Tv tv) {this.tv = tv;}@Overridepublic void run () {for(int i=0;i<100;i++){tv.voice="生产(演员)"+i;tv.play();}}}class Audience implements Runnable{/*** 操作tv对象*/Tv tv;public Audience (Tv tv) {this.tv = tv;}@Overridepublic void run () {for(int i=0;i<20;i++){tv.voice="消费(观众)"+i;tv.watch();}}}