1.进程
1.进程是正在运行的程序,也就是执行程序的一次执行过程,是系统进行资源分配的基本单位
2.目前操作系统都是支持多进程,可以进行执行多个进程,通过进程ID区分
3.单核cpu在同一时刻,只能有一个进程,宏观并行,微观串行。
2. 线程
线程,又称轻量级进程,进程中的一条执行路径,也是cpu的基本调度单位。一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程。
例如:迅雷是一个进程,当中的多个下载任务即为线程。
Java虚拟机是一个继承,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行
3.进程和线程的区别
1.进程是操作系统资源分配的基本单位,而线程是cpu的基本调度单位。
2.一个程序运行后至少有一个进程。
3.一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
4.进程间不能共享数据段地址,但是同进程的线程之间可以。
4.线程的组成
任何一个线程都具有基本的组成部分
CPU时间片: 操作系统(OS)会为每个线程分配执行时间
运行数据:
堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据。
栈空间: 存储线程需使用的局部变量,每个线程都拥有独立的栈。
线程的逻辑代码.
5.线程的特点
1. 线程抢占式执行
效率高
可防止单一线程长时间独占CPU.
2. 在单核CPU中,宏观上同时执行,微观上顺序执行
6.线程的创建方式(三种)
6.1 第一种方式: 继承Thread类 重写run方法
//1.继承线程
public class MyThread extends Thread{
//2。重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"==="+i);
}
}
}
public class Test {
public static void main(String[] args) {
//3.创建线程
MyThread myThread = new MyThread();
myThread.setName("aaa");
//4.开启线程
myThread.start();
}
}
相关方法:
一、获取线程ID和线程名称
1.在Thread的子类中调用this.getId()或this.getName()
2.使用Thread.currentThread().getId()和Thread.currentThread().getName()
二、修改线程名称
1. 调用线程对象的setName()方法
2. 使用线程子类的构造方法赋值
举例:使用线程Thread类实现4个窗口各卖100张票
public class MyThread extends Thread{
int ticket=100;
@Override
public void run() {
for(;ticket>0;ticket--){
System.out.println(Thread.currentThread().getName()+"剩余:"+ticket+"张票");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread m1 = new MyThread();
m1.setName("A窗口");
m1.start();
MyThread m2 = new MyThread();
m2.setName("B窗口");
m2.start();
MyThread m3 = new MyThread();
m3.setName("C窗口");
m3.start();
MyThread m4 = new MyThread();
m4.setName("D窗口");
m4.start();
}
}
若想让四个窗口共卖这100张票可在ticket前加 static 即static int ticket=100;
6.2 第二种方式: 实现Runnable接口
实例:实现四个窗口共卖100张票
public class MyRunnable implements Runnable{
int ticket=100;
@Override
public void run() {
for(;ticket>0;ticket--){
System.out.println(Thread.currentThread().getName()+"剩余:"+ticket+"张票");
}
}
}
public class TestDemo {
public static void main(String[] args) {
//可以将MyRunnable理解为一个任务由四个窗口执行,则共享票数 从而实现四个窗口
//共卖100张票
MyRunnable r1 = new MyRunnable();
Thread t1 = new Thread(r1,"A窗口");
Thread t2 = new Thread(r1,"B窗口");
Thread t3 = new Thread(r1,"C窗口");
Thread t4 = new Thread(r1,"D窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
6.3 第二种方式: 实现Callable接口
实现Callable接口,它和实现Runnable接口差不多,只是该接口种的方法有返回值和异常抛出。
public class Test {
public static void main(String[] args) throws Exception {
My1 task1=new My1();
My2 task2=new My2();
//第一种方式建创建线程对象并提交Callable类型的任务
//但是这种方式是比较麻烦的,需要封装到FutureTask类种,
//因此建议使用线程池来提交任务
FutureTask futureTask=new FutureTask(task1);
Thread t1=new Thread(futureTask);
t1.start();
System.out.println(futureTask.get());
//第二种方式 使用线程池来提交任务 应用场景: 适合大文件上传。
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Integer> future = executorService.submit(task1);
Integer sum1 = future.get();//需要等线程执行完毕后,才会把结果返回给该变量
//实现1-100的和
Future<Integer> future1 = executorService.submit(task2);
Integer sum2 = future1.get();
System.out.println(sum1+sum2);
}
}
class My1 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i=1;i<=50;i++){
sum+=i;
}
return sum;
}
}
class My2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum=0;
for (int i=51;i<=100;i++){
sum+=i;
}
return sum;
}
}
7.线程的生命周期以及状态转换
1.新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new Thread();
2.就绪状态(Runnable):当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3.运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4.阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞--运行状态中的线程执行 wait() 方法,使本线程进入到等待阻塞状态;
2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 -- 通过调用线程的 sleep() 或 join() 或发出了I/O请求时,线程会进入到阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期;
8.线程的常用方法
-
休眠:
public static void sleep(long millis) 当前线程主动休眠millis毫秒。
public class ThreadSleep {
public static void main(String[] args) {
ThreadSleepDemo t = new ThreadSleepDemo();
t.start();
for(int i=1;i<10;i++){
System.out.println("main线程====循环"+i+"次");
}
}
}
class ThreadSleepDemo extends Thread{
@Override
public void run() {
for(int i=1;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");
}
}
}
-
放弃:
public static void yield() 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片 yield()只让有相同执行权的线程获得cup时间片, 但是yield()不能控制cup交出的时间, yeild()只是让线程恢复到就绪状态, 那么可能在执行yeild()后进入就绪状态,然后马上又进入运行状态。
public class ThreadYield {
public static void main(String[] args) {
TY t1 = new TY();
t1.start();
for(int i=1;i<10;i++){
Thread.yield();
System.out.println("main线程===循环"+i+"次");
}
}
}
class TY extends Thread{
@Override
public void run() {
for(int i=1;i<10;i++){
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");
}
}
}
3.加入:
public final void join()
允许其他线程加入到当前线程中
在main函数线程中调用线程tj.join()方法,此时main函数线程就进入阻塞状态,
直到线程tj完全执行完以后,线程main才结束阻塞状态
public class ThreadJoin {
public static void main(String[] args) {
TJ tj = new TJ();
tj.start();
try {
//tj执行完后才会执行main函数
tj.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=1;i<10;i++){
System.out.println("main线程===循环"+i+"次");
}
}
}
class TJ extends Thread{
@Override
public void run() {
for(int i=1;i<10;i++){
System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");
}
}
}
4.优先级:
线程对象.setPriority()
线程优先级1-10,默认为5,优先级越高,表示获取CPU的概率越高
5.守护线程:
线程对象.setDaemon(true);设置为守护线程。
线程有两类:用户线程(前台线程)和守护线程(后台线程)
如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。
垃圾回收线程属于守护线程。
public class ThreadDeamon {
public static void main(String[] args) {
TP tp1 = new TP();
tp1.setName("A线程");
//当main线程执行完毕后,后台TP线程自动结束
tp1.setDaemon(true);
tp1.start();
for(int i=1;i<20;i++){
System.out.println("main线程===循环"+i+"次");
}
}
}
class TP extends Thread{
@Override
public void run() {
for(int i=1;i<50;i++){
System.out.println(Thread.currentThread().getName()+"===循环"+i+"次");
}
}
}
9.线程的安全问题
public class TestSafe {
//静态资源 共享
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));
//可能出现的情况,所以出现的情况会丢失 即数据不安全
// (1)hello world null null null
// (2)world hello null null null
// (3)hello null null null null
// (4)world null null null null
}
}
多线程安全问题:
1.当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
2.临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
3.原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
使用synchronized可以解决线程不安全问题
synchronized语法:
synchronized(临界资源对象){//对临界资源对象加锁
//代码 原子操作
}
public class TestSafe {
//静态资源 共享
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() {
//加锁 可以解决安全问题
synchronized (arr) {
//共享资源 原子操作
if (arr[index] == null) {
arr[index] = "hello";
index++;
}
}
}
};
Runnable world=new Runnable() {
@Override
public void run() {
synchronized (arr) {
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));
}
运行一直是这个
注意:
1.每个对象都有一个互斥锁标记,用来分配给线程
2.只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步代码块
3.线程退出同步代码块时,会释放相应的互斥锁标记
10.线程死锁
当A线程拥有锁资源a时,这时A线程需要锁资源b, 而B线程拥有锁资源b,这时B线程需要锁资源a, 这样会导致A等待B线程释放资源b, B线程等待A线程释放锁资源a。 从而二个处于永久等待。从而操作死锁。
例子: 两人去餐厅吃饭,A有一根筷子,B有另一个筷子。A要等B的那根筷子,B要等A的筷子,两个人都在循环等待,则会陷入死锁状态。
解决办法:使用synchronized可以解决该问题
public class Boy 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");
}
}
}
}
public class Gril 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");
}
}
}
}
public class LockObject {
/*筷子a*/
public static final Object a = new Object();
/*筷子b*/
public static final Object b = new Object();
}
public class Test {
public static void main(String[] args) {
Boy boy = new Boy();
boy.setName("aaa");
Gril gril = new Gril();
gril.setName("bbb");
boy.start();
gril.start();
}
}
1.得到死锁的原因:锁与锁之间有嵌套导致。
2.解决死锁的方法:
1. 尽量减少锁得嵌套。
2. 可以使用一些安全类。
3. 可以使用Lock中得枷锁,设置枷锁时间。
11.线程通信
所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。
因为我们无法对哪个线程先获得cpu,也无法确定哪个线程先执行,但是我们想指定某个线程先执行,哪些线程后执行,这样的话我们就需要线程通信技术。
线程通信中的方法:
1.等待 -- 释放锁,进入等待队列
public final void wait()
public finnal void wait(long timeout)
必须在对obj加锁的同步代码块中。
在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有的锁标记。同时此线程阻塞让其在等待队列中
属于Object类中的方法
2.通知 -- 唤醒等待队列中的线程,进入就绪队列中,参与cpu的竞争
public final void notify()
public final void notifyAll()
实例:存钱和取钱
规定-- 先存钱在取钱 且存一次钱 取一次钱
public class BankCard {
private double balance;
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 += money;
System.out.println(Thread.currentThread().getName()+"存了"+money+", 余额为:"+this.balance);
//存钱后将flag置于true 进入取钱操作
flag=true;
//唤醒当前等待队列中线程
this.notify();
}
//取钱 加入同步代码 使得线程安全
public synchronized void withdraw(double money){
//若还没有存钱 则让该线程等待
if(flag==false){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//否则 如下操作
this.balance -= money;
System.out.println(Thread.currentThread().getName()+"取了"+money+", 余额为:"+this.balance);
//取钱后将flag置于false 进入存钱操作
flag=false;
//唤醒当前等待队列中线程
this.notify();
}
}
public class BoyCard implements Runnable{
private BankCard bankCard;
public BoyCard(BankCard bankCard) {
this.bankCard = bankCard;
}
@Override
public void run() {
for (int i=0;i<10;i++){
bankCard.save(1000);
}
}
}
public class GrilCard implements Runnable{
private BankCard bankCard;
public GrilCard(BankCard bankCard) {
this.bankCard = bankCard;
}
@Override
public void run() {
for (int i=0;i<10;i++){
bankCard.withdraw(1000);
}
}
}
public class Test {
public static void main(String[] args) {
BankCard bankCard = new BankCard();
BoyCard boyCard = new BoyCard(bankCard);
GrilCard grilCard = new GrilCard(bankCard);
Thread t1 = new Thread(boyCard,"AA");
Thread t2 = new Thread(grilCard,"BB");
t1.start();
t2.start();
}
}
sleep和wait的区别:
1.所在的类不同:sleep属于Thread类,wait属于Object类。
2.使用的地方: sleep可以在任何代码块中使用。wait只能在同步代码块中使用。
3.锁资源的释放: sleep不释放锁资源,wait会释放锁资源。
4.sleep时间片到了自动唤醒,wait必须需要使用notify或notifyAll唤醒
notify()和 notifyAll()有什么区别?
1.notifyl()会唤醒所有的线程,notify()会唤醒一个线程。
2.notifyAll() 会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
12.线程池
该池子中预先存储若干个线程对象。整个池子就是线程池
存在问题:
1.线程是宝贵的内存资源,单个线程约占1MB的空间,过多分配易造成内存溢出。
2.频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。
线程池的作用:
1.线程容器,可设定线程分配的数量上限
2.将预先创建的线程对象存入池子中,并重用线程池中的线程对象
3.避免频繁的创建和销毁。
线程池的创建方式:
所有的线程池—封装了一个父接口—java.util.concurrent.Executor.
它的实现接口: ExecutorService.
工具类Executors可以创建相应的线程池:
[1] 创建单一线程池 newSingleThreadExecutor()
[2] 创建定长线程池。newFixedThreadPool(n);
[3] 创建可变线程池. newCachedThreadPool()
[4] 创建延迟线程池 .newScheduledThreadPool(n);
方法:
Executor:线程池的根类.它中的方法execute()执行线程任务的方法Runnable类型的任务
ExecutorService: 线程池的子接口
shutdown(); 关闭线程池。需要等待线程池中任务执行完毕后才会关闭。
shutdownNow(): 立即关闭线程池。
isTerminated():判断线程池是否终止了。
submit(): 提交任务给线程池中线程对象、Runnable和Callable类型的任务。
- 创建单一线程池 newSingleThreadExecutor()
适应场景:队列要求线程有序执行。
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 5; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "====");
}
});
executorService.shutdown();
}
不管存在几个线程 永远都是一个线程在执行五次
- 创建定长线程池。newFixedThreadPool(n);
ExecutorService executorService = Executors.newFixedThreadPool(2);
for(int i=1;i<=5;i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "====" );
}
});
executorService.shutdown();
}
- 创建可变线程池. newCachedThreadPool()
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=1;i<=5;i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "====" );
}
});
executorService.shutdown();
}
- 创建延迟线程池 .newScheduledThreadPool(n);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
for(int i=1;i<=5;i++){
System.out.println(Thread.currentThread().getName()+"===="+i);
}
}
},10, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();
}
//long delay:延迟时间 TimeUnit unit:时间单位,Runnable command:runnable的创建线程
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
推荐使用原始的创建线程池方式
上面使用Executors创建线程池的四种放方式,都是基于底层ThreadPoolExecutor实现,而阿里开发手册,建议使用最原始的方式。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险
/*原始*/
/*int corePoolSize, 核心线程数
* int maximumPoolSize, 最大线程数
* long keepAliveTime, 空闲时间
* TimeUnit unit, 时间单位
* BlockingQueue<Runnable> workQueue: 堵塞队列,
* LinkedBlockingDeque:可以设置等待的个数,如果不设置默认为Integer的最大值。*/
LinkedBlockingDeque blockingDeque = new LinkedBlockingDeque(3);
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 10, TimeUnit.SECONDS, blockingDeque);
/*这里注意 要保证循环的次数-阻塞队列线程数<= 最大线程数 否则存在异常*/
for (int i = 0; i < 8; i++) {
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"===");
}
});
}
//关闭线程
executor.shutdown();
}
13.手动锁
Lock是手动锁的父接口,它下面有很多实现类。
lock() :获取锁,如锁被占用,则等待
unlock()释放锁资源,放在finally中
boolean tryLock():尝试获取锁(成功返回true,失败返回false,不阻塞)
使用重入锁解决卖票
public class demo04 {
public static void main(String[] args) {
Ticket task=new Ticket();
Thread t1=new Thread(task,"窗口A");
Thread t2=new Thread(task,"窗口B");
Thread t3=new Thread(task,"窗口C");
Thread t4=new Thread(task,"窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket implements Runnable{
private int ticket=100;
Lock s=new ReentrantLock();
@Override
public void run() {
while(true) {
try {
s.lock();//查看释放获取锁资源
if (ticket > 0) {
--ticket;
System.out.println(Thread.currentThread().getName() + "卖了一张,剩余:" + ticket + "张");
} else {
break;
}
}finally {
s.unlock();//释放锁
}
}
}
}
Synchronized和Lock区别:
1.synchronized可以给类,方法,代码块加锁,而lock只能给代码块加锁
2.synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而lock需要自己加锁和释放锁,如果使用不当没有unlock()去释放锁就会造成死锁
3.通过lock可以知道有没有成功的获取锁,而synchronized却无法办法