1、程序、进程与线程
程序(program)是为完成特定任务、用某种语言编写的一组指令的结合。即指一段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过称。——生命周期。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread)
进程可以进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
单核CPU与多核CPU。单核CPU,其实就是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过。如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
一个Java应用程序java.exe,其实至少有三个线程:mian()主线程,gc()垃圾回收线程,异常处理线程。
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀,多个人做同一件事。
使用多线程的优点?
一单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程的优点:
1、提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2、提高计算机系统CPU的利用率。
3、改善程序结构。将即长又复杂的进程分为多个线程,独立运行,利于理解与修改。
何时需要多线程?
程序需要同时执行两个或多个任务。
程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
需要一些后台运行的程序时。
2、线程的创建与使用
在JDK1.5之前创建新执行线程的方式有两种:
(1)继承Thread类
(2)实现Runnable接口
(3)线程的启动
通过继承Thread类创建线程实例
public class TestThread1 extends Thread{
@Override
public void run() {
System.out.println("执行run()");
super.run();
}
public static void main(String[] args) {
TestThread1 t1= new TestThread1();
t1.start();//开启线程,由CPU调度线程的执行。这个执行过程是随机的,不可人为干预的。
System.out.println("执行main()");//main方法执行体
}
}
通过实现Runnable接口创建线程实例
//实现Runnable接口创建接口
public class TestThread3 implements Runnable{
public void run() {
System.out.println("执行代理线程");
}
public static void main(String[] args) {
TestThread3 testThread3 = new TestThread3();
Thread t1 = new Thread(testThread3);
t1.start();
}
}
线程的启动
每个线程都是通过对应的run方法进行操作的,我们把run()方法的主体称为线程体。线程是通过Thread对象的start()方法来启动,而非直接调用run()方法,调用start()方法后系统会自动帮我们执行run方法的内容。
需知:run()方法由JVM调用,什么时候调用,执行的过程控制都由操作系统的CPU调度决定。如果自己手动调用run方法,那么就只是普通方法,没有启动多线程模式。一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IIIegalThreadStateException”。
3、线程的生命周期(五大状态)
线程的五大状态为:创建状态 就绪状态 阻塞状态 运行状态 死亡状态
状态流程图
4、线程的执行控制
4.1、线程礼让(重新排队)
使用yield()方法可以使当前线程回到就绪状态,进入待机的队列,CPU开始新一轮的调度。
public class TestYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始");
Thread.yield();//使当前执行线程回到就绪状态,让CPU重新调度。(随机)
System.out.println(Thread.currentThread().getName() + "停止");
}
public static void main(String[] args) {
new Thread(new TestYield(),"t1").start();
new Thread(new TestYield(),"t2").start();
}
}
该方法会让当前线程重新进入就绪状态,让CPU重新调度,但不一定会礼让。
4.2、线程优先(强行插队)
使用join()方法可以使原来的并行执行变为串行,进入join方法后的线程被视为特殊线程,会优先执行完成。且只有此特殊线程执行完毕之后,其它线程才可获取CPU资源。
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("run()线程:" + i);
}
}
public static void main(String[] args) throws InterruptedException {
//t1线程
Thread t1 = new Thread(new TestJoin(), "t1");
t1.start();
//main线程
for (int i = 0; i < 3; i++) {
System.out.println("main:" + i);
if(i == 1){
t1.join();
}
}
}
}
join()方法真正的让当前线程优先执行。
4.3、线程的优先级设置(修改线程获取CPU资源的概率)
Java提供线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该哪个线程来
执行。线程优先级有1-10
Thread.MIN_PRIORITY=1;
Thread.NORM_PRIORITY=5;
Thread.MAX_PRIORITY=10;
本质上是应该是优先级越大,优先权利越重。但实际上最终线程执行的优先级还是由CPU来决定的,只是优先级越高优先执行的概率更高而已。
public class TestPriority {
public static void main(String[] args) {
Runnable r = ()->{
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
};
Thread t1 = new Thread(r,"t1");
Thread t2 = new Thread(r,"t2");
t1.setPriority(Thread.MAX_PRIORITY);
//t1线程设置最高优先级
t2.setPriority(Thread.MIN_PRIORITY);//t2线程设置最低优先级
t1.start();
t2.start();
}
}
给线程显式的标注了优先级,但实际上执行的顺序还是得看CPU心情。
5、线程同步
下面是线程同步相关内容,点击后面的 # 号即可查看详细内容。
线程在并发的时候可能存在安全隐患:
(1)线程不安全案例(一)买票 #
(2)线程不安全案例(二)银行取钱 #
(3)线程不安全案例(三)集合 #
我们可以给访问数据的方法加锁,来保证资源的同步,以此来解决多线程并发下可能发生的问题。
(1)线程同步 sychronized方法 #
(2)线程同步 sychronized块 #
(3)CopyOnWriteArrayList,JUC线程安全的集合 #
(4)强大的可灵活添加、释放的锁Lock #
同步锁下可能出现的问题
死锁 #
public class UnsafeTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"p1").start();
new Thread(buyTicket,"p2").start();
new Thread(buyTicket,"p3").start();
}
}
class BuyTicket implements Runnable{
private int ticketNum = 2;//票的数量
private boolean flag = true;//是否还有票
@Override
public void run() {
//买票
if (flag){
buy();
}
}
private void buy(){
if(ticketNum < 1){
flag = false;
return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到最后第" + ticketNum-- + "张票");
}
}
package sychronize;//不安全的取钱//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"卡名");
new Thread(new Drawing(account,100)).start();
new Thread(new Drawing(account,100)).start();
new Thread(new Drawing(account,100)).start();
}
}
//账户
class Account{
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
package sychronize;//不安全的取钱//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"卡名");
new Thread(new Drawing(account,100)).start();
new Thread(new Drawing(account,100)).start();
new Thread(new Drawing(account,100)).start();
}
}
//账户
class Account{
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account;//账户
//取款金额
int drawingMoney;
public Drawing(Account account,int drawingMoney){
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override public void run() {
//判断有没有钱
if(account.money - drawingMoney < 0){
System.out.println("余额不足!!!您余额还有" + account.money);
return;
}
//sleep可以放大问题的发生性
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - drawingMoney;
System.out.println("取款成功。剩余余额" + account.money);
}
}
//线程不安全的集合
public class UnsafeList {
static List<String> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(()->{
UnsafeList.list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(3000);
System.out.println(list.size());
}
}
线程同步 synchronized方法,同一个对象下访问方法同步
synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法结束才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。同步锁还存在一个缺陷,若将一个大的方法申明为synchronized将会影响效率,故我们只需要锁住需要修改的资源。一些只读的的资源可以不加锁。
//给方法上加锁,锁的是this就是同一个对象的方法。
private synchronized void buy(){
if(ticketNum < 1){
flag = false;
return;
}
try {
Thread.sleep(100);//添加延时,可以更明显的看到线程错误
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到最后第" + ticketNum-- + "张票");
}
在方法上加synchronized关键字,可以使同一个对象下访问该方法同步。需要注意的是,当一个线程获得同步方法时,实际是锁住了整个当前对象。这时如果有其它线程想要访问该对象下其它同步方法也不允许的,需要等待占锁线程释放对象才可以使用。
线程同步 sychronized 同步块,在不同对象下访问方法同步
//不安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(1000,"卡名");
new Thread(new Drawing(account,100)).start();
new Thread(new Drawing(account,100)).start();
new Thread(new Drawing(account,100)).start();
}
}
//账户
class Account{
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
//账户
Account account;
//取款金额
int drawingMoney;
public Drawing(Account account,int drawingMoney){
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//同步块,可以锁任何对象
synchronized (account){
//判断有没有钱
if(account.money - drawingMoney < 0){
System.out.println("余额不足!!!您余额还有" + account.money); return;
}
//sleep可以更加直观的看到线程错误
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - drawingMoney;
System.out.println("取款成功。剩余余额" + account.money);
}
}
}
使用同步块可以锁任何对象,但是注意一定是要锁要修改的对象。
CopyOnWriteArrayList,JUC线程安全的集合
import java.util.concurrent.CopyOnWriteArrayList;
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
更加强大的线程同步机制 Lock锁
Lock可以显式的定义同步锁对象来实现同步。ReentrantLock可以显式的加锁、释放锁。
package lock;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
public class TestLock {
public static void main(String[] args) {
TestLock2 t2 = new TestLock2();
for (int i = 0; i < 20; i++) {
new Thread(t2).start();
}
}
}
class TestLock2 implements Runnable{
int ticketNums = 10; //定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();//加锁
if(ticketNums > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}
lock.unlock();//解锁
}
}
使用lock锁可以显示(手动)的加锁与释放锁,注意一定要记得释放锁。使用lock锁,JVM调度线程花费的时间更少,性能更优。故想要线程同步优先考虑的顺序应该是:Lock > 同步代码块(锁部分代码)> 同步方法(锁整个方法)
死锁
多个线程互相抱着对方需要的资源,然后形成僵持的情况称为死锁。
public class DeadLock {
public static void main(String[] args) {
Makeup m1 = new Makeup(0,"灰姑娘");
Makeup m2 = new Makeup(1,"白雪公主");
m1.start();
m2.start();
}
}
//口红
class Lipstick{}
//镜子
class Mirror{
}
class Makeup extends Thread{
//需要的资源只有一份,用static来保证只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;//选择
String girlName;//使用化妆品的人
Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆,相互持有对方的锁,就是需要拿到对方的资源
private void makeup() throws InterruptedException{
if(choice == 0){
synchronized (lipstick){//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror){
//一秒钟后向获得镜子
System.out.println(this.girlName + "获得镜子的锁");
}
}
}else {
synchronized (mirror){//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick){
//一秒钟后向获得镜子
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}
出现死锁的原因是对方都抱有对方所需要的资源(锁住了),并且都不肯放手(释放锁)。解决这个问题的方法就是一方先释放对方需要的资源。
6、线程之间的通信 生产者与消费者案例
线程之间的通信:wait()与notify()和notifyAll()方法
- wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可以访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notigyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
- notifyAllI():唤醒正在排队等待资源的所有线程结束等待。
注意:这三个方法只有在sychronized方法或sychronized代码块中才能使用,否则会报java.lang.IIIegalMonitorStateException异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
线程通信:生产者与消费者案例
程序流程:生产者生产产品——通知消费者拿走——消费者拿走产品——通知生产者生产
(1)信号灯法,标志位解决
package deal;//测试生产者消费者问题2:信号灯法,标志位解决
public class TestDeal2 {
public static void main(String[] args) {
Food f = new Food();
new Canteen(f).start();
new Customer(f).start();
}
}
//生产者-->施粥者
class Canteen extends Thread{
Food food;
public Canteen(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i = 0; i < 4; i++) {
food.serving("面包");
}
}
}
//消费者-->食客
class Customer extends Thread{
Food food;
public Customer(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i = 0; i < 4; i++) {
food.eat();
}
}
}
//产品(粥)
class Food{
//拍摄电视,观众等待
//观众观看,演员休息
String food;//食物
boolean flag = true; //上菜
public synchronized void serving(String food){
if(!flag){
try {
System.out.println("服务员等待...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知食客食用
System.out.println("服务员上了一碗" + food);
this.food = food;
this.flag = !this.flag;
this.notifyAll();//通知唤醒
}
//食客食用
public synchronized void eat(){
if(flag){
try {
System.out.println("顾客等待...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("食客吃完了" + food); //通知餐厅上菜
this.flag = !this.flag;
this.notifyAll();
}
}
使用同步锁锁住对象的使用,wait()让其它线程进入等待状态,notifyAll()唤醒等待的线程,加上一个flag标志位做判断。如此就实现了线程通信。–线程通信的方法wait() 线程一直等待,直到其他线程通知,与sleep不同,会释放锁。wait(long timeout) 指定等待的毫秒数notify() 唤醒一个处于等待状态的线程notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别越高的线程优先调度
(2)管程法
package com.kuang;
public class TestProduce {
public static void main(String[] args) {
Systack sy = new Systack();
Shengchan sc = new Shengchan(sy);
XiaoFei xf = new XiaoFei(sy);
new Thread(xf).start();
new Thread(sc).start();
}
}
class ManTou {
int id;
ManTou(int x) {
id = x;
}
public ManTou() {
// TODO Auto-generated constructor stub
}
}
class Systack {
int index = 0;
ManTou[] mt = new ManTou[10];
public synchronized void push(ManTou m) {
while (index == mt.length) {
try {
this.wait();//释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();//
mt[index] = m;
index++;
}
public synchronized ManTou pop() {
while (index == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
index--;
return mt[index];
}
}
class XiaoFei implements Runnable {
Systack sc = null;
public void run() {
for (int i = 0; i < 20; i++) {
ManTou m = sc.pop();
System.out.println("吃馒头");
}
}
public XiaoFei(Systack sc) {
super();
this.sc = sc;
}
}
class Shengchan implements Runnable {
Systack sc = null;
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("造馒头");
ManTou mt = new ManTou(i);
sc.push(mt);
}
}
public Shengchan(Systack sc) {
super();
this.sc = sc;
}
}
7、线程池
系统中经常创建和销毁使用量特别大的资源,如在并发情况下的线程,对性能影响很大。可以使用线程池提前创建好的线程,使用时直接获取,用完放回池中。即可避免频繁创建销毁,实现重复利用。
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;//测试线程池
public class TestPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(3);
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
使用线程池可以减少系统性能的损耗。
8、守护线程
线程分为用户线程与守护线程。虚拟机必须确保用户线程执行完毕,不用等待守护线程执行完毕。守护线程有:后台记录操作日志,监控内存,垃圾回收等…守护线程用于守护用户线程,且会在用户线程执行完毕后结束。
//守护线程
public class TestDaemon {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守护线程执行...");
}
});
Thread userThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("用户线程执行:" + i);
}
});
daemonThread.setDaemon(true);
daemonThread.start();
userThread.start();
}
}