目录
1.基本原理
1.1 何为进程
正在运行的程序,是系统进行资源分配的基本单位
目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分
单核CPU在同一时刻,只能有一个进程;宏观并行,微观串行
1.2 何为线程
线程,又称轻量级进程(Light Weight Process)。进程中的一条执行路径,也是CPU的基本调度单位。一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程。
例如:迅雷是一个进程,当中的多个下载任务即为线程;Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行
1.3 两者区别
1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
2. 一个程序运行后至少有一个进程。
3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
4. 进程间不能共享数据段地址,但是同进程的线程之间可以。
2.线程
2.1 线程的组成
任何一个线程都具有基本的组成部分
CPU时间片: 操作系统(OS)会为每个线程分配执行时间
运行数据:
堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据。
栈空间: 存储线程需使用的局部变量,每个线程都拥有独立的栈。
线程的逻辑代码。
2.2 线程的特点
1. 线程抢占式执行
效率高
可防止单一线程长时间独占CPU
2. 在单核CPU中,宏观上同时执行,微观上顺序执行
2.3 线程的创建方式
1. 【继承Thread类,重写run方法】
2. 【实现Runnable接口】
3. 实现Callable接口
2.3.1 获取和设置线程的名称
1.获取线程ID和线程名称
(1) 在Thread的子类中调用this.getId()或this.getName()
(2) 使用Thread.currentThread().getId()和Thread.currentThread().getName()
2.修改线程名称
(1)调用线程对象的setName()方法
(2)使用线程子类的构造方法赋值
2.3.2 继承Thread类
方法:
public class ThreadTest extends Thread{
@Override
public void run() {
//写自己的任务代码
for (int i = 0; i < 20; i++) {
//1通过Thread对象中getName();有缺点只能在Thread子类中获取
//System.out.println(this.getName()+"======================="+i);
//2在Thread类中存在一个静态方法可以获取当前线程对象,再通过该对象调用getName()
System.out.println(Thread.currentThread().getName()+"==============="+i);
}
}
}
测试类:
public class MainTest {
public static void main(String[] args) {
//创建一个线程对象
ThreadTest threadTest=new ThreadTest();
threadTest.setName("副线程");//设置线程名称
//开启线程---执行时会调用run方法 时间片
threadTest.start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程----------------"+i);
}
}
}
2.3.3 实现Runnable接口
任务代码:
public class MyRunnable implements Runnable {
//任务代码
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"============"+i);
}
}
}
测试类:
public class RunnableTest {
public static void main(String[] args) {
//1.创建任务对象
MyRunnable myRunnable=new MyRunnable();
//2.创建线程对象并执行要执行的任务 可直接命名 myRunnable此处必写上边对应的创建任务对象名字
Thread t1=new Thread(myRunnable,"副线程");
t1.start();
for (int i = 0; i < 20; i++) {
System.out.println("main线程---------------------"+i);
}
}
}
2.4 线程的状态
2.5 常见方法
2.5.1 休眠(sleep)
public static void sleep(long millis)
当前线程主动休眠millis毫秒。
例子:实现一个时间显示,每隔一秒钟更新一次显示。例如时间秒数显示
public class SleepTest {
public static void main(String[] args) {
DateFormat date=new SimpleDateFormat("yyyy-MM-dd E HH:mm:ss");//自定义时间模式
while (true){
Date d=new Date();//获取当前时间
String format = date.format(d);
System.out.println(format);
try {
Thread.sleep(1000);//当前线程主动休眠1秒进行打印 此处有异常要抛出
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.5.2 放弃(yield)
public static void yield()
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片线程;调用yield会让出cpu的执行权,但别的线程不一定接受礼让。在cpu比较繁忙的时候效果明显,空闲的时候没什么影响。
例子:A B两个线程,不断对i进行输出,B执行yield方法,B线程的i出现次数频率明显减少。没有yield方法的话,A B两个线程的出现频率次数差不多
public class YieldTest {
public static void main(String[] args) {
A a=new A();
B b=new B();
a.setName("A");
b.setName("B");
a.start();
b.start();
}
}
class A extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"======"+i);
}
}
}
class B extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
Thread.yield();
System.out.println(Thread.currentThread().getName()+"============="+i);
}
}
}
2.5.3 加入(join)
public final void join()
允许其他线程加入到当前线程中
例子:
在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行
join方法必须在线程start方法调用之后调用才有意义
join方法可以在start方法前调用时,并不能起到同步的作用
开启多条线程同时爬取不同网站的信息,进行处理,然后统一返回给前台,这里面要注意的就是,我们必须等待前面的线程都执行结束,才能返回给前端
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Runnable a=new Runnable() {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
System.out.println("A:"+i);
}
}
};
Runnable b=new Runnable() {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
System.out.println("B:"+i);
}
}
};
Thread t1=new Thread(a);
Thread t2=new Thread(b);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("爬取结束");
}
}
2.5.4 守护线程(setDaemon)
线程对象.setDaemon(true);设置为守护线程。
线程有两类:用户线程(前台线程)和守护线程(后台线程)
如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。
垃圾回收线程属于守护线程。
举例: 将一个线程以守护线程方式来运行,即主线程已经执行完毕,守护线程程序也结束
public class DaemonTest {
public static void main(String[] args) throws InterruptedException {
Daemon d=new Daemon();
Thread t=new Thread(d);
t.setDaemon(true);
t.start();
Thread.sleep(3000);
System.out.println("线程结束");
}
}
class Daemon implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("运行副线程"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.6 线程的状态
3.线程安全
3.1 线程安全问题
代码展示:
public class Safe {
private static String [] arr=new String[5];
private static int index=0;
public static void main(String[] args) throws Exception {
//匿名函数
Runnable hello=new Runnable() {
@Override
public void run() {
if(arr[index]==null){
arr[index]="hello";
index++;
}
}
};
//匿名函数
Runnable world=new Runnable() {
@Override
public void run() {
if(arr[index]==null){
arr[index]="world";
index++;
}
}
};
Thread t1=new Thread(hello);
Thread t2=new Thread(world);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Arrays.asList(arr));
//此处结果有4种
//[hello, world, null, null, null]
//[world, hello, null, null, null]
//[hello, null, null, null, null]
//[world, null, null, null, null]
}
}
出现问题
(1)当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
(2)临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
(3)原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
3.2 解决方法
3.2.1 同步代码块
synchronized(临界资源对象){//临界资源对象加锁
//代码(原子操作)
}
注意:
每个对象都有一个互斥锁标记,用来分配给线程
只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块
线程退出同步代码块时,会释放相应的互斥锁标记
举例1:四个窗口分别卖100张票
public class Buy extends Thread{
private static int ticked=100;
@Override
public void run() {
while (true){
if(ticked>0){
System.out.println(this.getName()+"正在卖第"+ticked+"张票");
ticked--;
}else{
break;
}
}
}
}
测试类
public class BuyTest {
public static void main(String[] args) {
Buy buy1=new Buy();
buy1.setName("窗口1");
Buy buy2=new Buy();
buy2.setName("窗口2");
Buy buy3=new Buy();
buy3.setName("窗口3");
Buy buy4=new Buy();
buy4.setName("窗口4");
buy1.start();
buy2.start();
buy3.start();
buy4.start();
}
}
可能会出现负数,多线程共享一个资源时就有可能出现线程安全问题
举例2:四个窗口共卖100张票
(1)使用Runable
public class BuyOnly implements Runnable{
private static int ticked=100;
@Override
public void run() {
while (true){
synchronized (this){//必须保证多个线程使用的是同一个锁,此处不能new一个对象,ticked不是一个对象
if(ticked>0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticked+"张票");
ticked--;
try{
Thread.currentThread().sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
测试类
public class BuyOnlyTest {
public static void main(String[] args) {
BuyOnly buyOnly=new BuyOnly();
Thread thread=new Thread(buyOnly,"窗口A");
Thread thread1=new Thread(buyOnly,"窗口B");
Thread thread2=new Thread(buyOnly,"窗口C");
Thread thread3=new Thread(buyOnly,"窗口D");
thread.start();
thread1.start();
thread2.start();
thread3.start();
}
}
(2)使用Thread
public class BuyOnlyThread extends Thread{
private static int ticked=100;
@Override
public void run() {
while(true){
//使用本类作为锁对象
synchronized (BuyOnlyThread.class){
if(ticked>0){
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(this.getName()+"正在卖第"+ticked+"张票");
ticked--;
}else{
break;
}
}
}
}
}
测试类
public class BuyOnlyThreadTest {
public static void main(String[] args) {
BuyOnlyThread buyOnlyThread=new BuyOnlyThread();
buyOnlyThread.setName("A窗口");
BuyOnlyThread buyOnlyThread1=new BuyOnlyThread();
buyOnlyThread1.setName("B窗口");
BuyOnlyThread buyOnlyThread2=new BuyOnlyThread();
buyOnlyThread2.setName("C窗口");
BuyOnlyThread buyOnlyThread3=new BuyOnlyThread();
buyOnlyThread3.setName("D窗口");
buyOnlyThread.start();
buyOnlyThread1.start();
buyOnlyThread2.start();
buyOnlyThread3.start();
}
}
3.2.2 线程的等待
3.2.3 同步方法
synchronized 返回值类型 方法名称(形参列表){//对当前对象(this)加锁
//代码(原子操作)
}
注意:
只有拥有对象互斥锁的标记的线程,才能进入该对象加锁的同步方法中
线程退出同步方法时,会释放相应的互斥锁标记
4.死锁
死锁:当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁
一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁
出现原因:锁的嵌套
举例:吃饭时需要一双筷子,现在有两人一人分一根筷子,如何完成吃饭
定义锁的类型
public class LockObject {
public static Object a=new Object();
public static Object b=new Object();
}
定义Boy,Girl类
public class Boy extends Thread{
@Override
public void run() {
synchronized (LockObject.a){
System.out.println(Thread.currentThread().getName()+"获取一根筷子a");
synchronized (LockObject.b){
System.out.println(Thread.currentThread().getName()+"获取一根筷子b");
System.out.println("可以吃饭了");
}
}
}
}
public class Girl extends Thread{
@Override
public void run() {
synchronized (LockObject.b){
System.out.println(Thread.currentThread().getName()+"获取一根筷子b");
synchronized (LockObject.a){
System.out.println(Thread.currentThread().getName()+"获取一根筷子a");
System.out.println("可以吃饭了");
}
}
}
}
定义测试类
public class DeadLock {
public static void main(String[] args) {
Boy b=new Boy();
Girl g=new Girl();
b.setName("Tom");
g.setName("Marry");
b.start();
g.start();
}
}
5.线程通信
两者都不是在Thread类中,而是在Object类中
等待:
public final void wait()
public final void wait(long timeout)
必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
通知:
public final void notify()
public final void notifyAll()
唤醒等待队列中线程,进入就绪队列,参与CPU的竞争
举例:男友存钱,女友取钱问题,必须先存才可以取钱
银行卡类
public class BankCard {
private double balance;
//false代表没有钱 true代表有钱
private boolean flag=false;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//存钱
public synchronized void save(double money){
if(flag==true){
//进入等待队列
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//进入存钱
this.balance=this.balance+money;
System.out.println(Thread.currentThread().getName()+"存入"+money+",余额为:"+this.balance);
flag=true;
//唤醒等待队列的线程
this.notify();
}
//取钱
public synchronized void take(double money){
if(flag==false){
//进入等待队列
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//进入取钱
this.balance=this.balance-money;
System.out.println(Thread.currentThread().getName()+"取出"+money+",余额为:"+this.balance);
flag=false;
//唤醒等待队列的线程
this.notify();
}
}
定义男,女主
public class Boy extends Thread{
private BankCard bankCard;
public Boy(BankCard bankCard){
this.bankCard=bankCard;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bankCard.save(1000);
}
}
}
public class Girl extends Thread{
private BankCard bankCard;
public Girl(BankCard bankCard){
this.bankCard=bankCard;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bankCard.take(1000);
}
}
}
定义测试类
public class MoneyTest {
public static void main(String[] args) {
BankCard bankCard=new BankCard();
Boy b=new Boy(bankCard);
Girl g=new Girl(bankCard);
b.setName("Tom");
g.setName("Marry");
b.start();
g.start();
}
}
6.线程池
6.1 何为线程池
出现的问题:
(1)线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。
(2)频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。
线程池:
(1)线程容器,可设定线程分配的数量上限。
(2)将预先创建的线程对象存入池中,并重用线程池中的线程对象。
(3)避免频繁的创建和销毁。
6.2 常用方法
Executor:线程池的根类。里面有一个方法。execute执行线程任务的方法Runnable类型的任务
ExecutorService:线程池的子接口
shutdown():关闭线程池。需要等待线程池中任务执行完毕后才会关闭
shutdownNow():立即关闭线程池
isTerminated():判断线程池是否 终止了
submit():提交任务给线程池中线程对象。Runable和Callable类型的任务
6.3 线程池方法的使用
public class CreatMethods {
public static void main(String[] args) {
//单一线程池 使用场景:队列要求线程有序执行
//ExecutorService executorService = Executors.newSingleThreadExecutor();
//创建固定长度的线程池对象
//ExecutorService executorService = Executors.newFixedThreadPool(3);
//创建可变长度的线程池 不会根据i输出数字一致,如果是100可能就会创建40多个
//ExecutorService executorService = Executors.newCachedThreadPool();
//创建延迟线程池对象
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
//前三个方法使用
// for (int i = 0; i < 5; i++) {
// executorService.submit(new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName()+"=========================");
// }
// });
for (int i = 0; i < 100; i++) {
executorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"=========================");
}
},10, TimeUnit.SECONDS);
}
executorService.shutdown();
}
}
6.4 通过自定义阻塞队列创建线程池
上面讲解的使用Executors创建线程池的方式,都是使用底层ThreadPoolExecutor,而阿里开发手册,建议使用最原始的方式。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
* int corePoolSize,核心线程数 * int maximumPoolSize,最大线程数 * long keepAliveTime,空闲时间 * TimeUnit unit,时间单位 * BlockingQueue<Runnable> workQueue 堵塞队列
public class CreatMethodsTest {
public static void main(String[] args) {
//LinkedBlockingDeque:可以设置等待的个数,如果不设置默认为Integer的最大值
BlockingQueue blockingQueue=new LinkedBlockingDeque(3);
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,5,10, TimeUnit.SECONDS,blockingQueue);
for (int i = 0; i < 5; i++) {
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"---------");
}
});
}
threadPoolExecutor.shutdown();
}
}
7.创建线程的第三种方式
实现Callable接口,它和实现Runnable接口差不多,只是该接口种的方法有返回值和异常抛出。 比较适用于大文件的上传。
举例:进行1-100的值相加
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//自创建线程对象并提交Callable类型的任务是比较麻烦的,需要封装到一个FutureTask类种
//比较麻烦,建议使用线程池
// My task=new My();
// FutureTask futureTask=new FutureTask(task);
// Thread t1=new Thread(futureTask);
// t1.start();
// System.out.println(futureTask.get());
My t=new My();
My2 t2=new My2();
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Integer> submit = executorService.submit(t);
Future<Integer> submit2 = executorService.submit(t2);
Integer integer = submit.get();//需要等待线程执行完毕后,才会把结果返回该变量
Integer integer2 = submit2.get();//需要等待线程执行完毕后,才会把结果返回该变量
System.out.println(integer+integer2);
executorService.shutdown();
}
}
class My implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i <=100; i++) {
sum+=i;
}
return sum;
}
}
class My2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum2=0;
for (int i = 0; i <=100; i++) {
sum2+=i;
}
return sum2;
}
}
8.手动锁
举例:四个窗口买票
8.1 Thread方法手动锁
public class LockTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
TicketTest ticketTest=new TicketTest();
ticketTest.setMyLock(lock);//注意此处的锁,每new一次都会有一把新锁,要使用同一把锁
ticketTest.setName("窗口A");
TicketTest ticketTest1=new TicketTest();
ticketTest1.setMyLock(lock);
ticketTest1.setName("窗口B");
// TicketTest ticketTest2=new TicketTest();
// ticketTest2.setName("窗口C");
// TicketTest ticketTest3=new TicketTest();
// ticketTest3.setName("窗口D");
ticketTest.start();
ticketTest1.start();
// ticketTest2.start();
// ticketTest3.start();
}
}
class TicketTest extends Thread{
private static int ticket=100;
//定义手动锁
Lock myLock;
public Lock getMyLock() {
return myLock;
}
public void setMyLock(Lock myLock) {
this.myLock = myLock;
}
@Override
public void run() {
try{myLock.lock();//上锁
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+--ticket+"张");
}else {
break;
}
}
}finally {
myLock.unlock();//开锁
}
}
}
8.2 Runnable方法手动锁
public class RunnableLock {
public static void main(String[] args) {
RunnableTest runnableTest=new RunnableTest();
Thread t1=new Thread(runnableTest,"窗口A");
Thread t2=new Thread(runnableTest,"窗口B");
Thread t3=new Thread(runnableTest,"窗口C");
Thread t4=new Thread(runnableTest,"窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class RunnableTest implements Runnable{
private static int ticket=100;
Lock myLock=new ReentrantLock();
@Override
public void run() {
while(true){
try{
// myLock.lock();//要是放在while(true)上边,相当于对整个while锁住,只能有一个窗口进入
//boolean b=myLock.tryLock();//查看是否获取锁资源
myLock.lock();
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket--+"张票");
}else {
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
break;
}
}finally {
myLock.unlock();
}
}
}
}