1.什么是线程
线程,又称轻量级进程(Light Weight Process)。 进程中的一条执行路径,也是CPU的基本调度单位。 一个进程由一个或多个线程组成,彼此间完成不同的工作, 同时执行,称为多线程。
迅雷是一个进程,当中的多个下载任务即为线程。
Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。
public class Test1 {
public static void main(String[] args) {
My my = new My();
//为线程 创建名字
my.setName("wmd");
//开启 线程
my.start();
//主线程的代码块
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---------------");
}
}
//写一个类继承 Thread
static class My extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//Thread.currentThread().getName 获得线程的名字
System.out.println(Thread.currentThread().getName()+"************");
}
}
}
}
由控制台输出的情况可以看出 进程和主线程任务是交替执行的
2.进程和线程的区别
1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
2. 一个程序运行后至少有一个进程。
3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
4. 进程间不能共享数据段地址,但是同进程的线程之间可以。
3.线程的组成
任何一个线程都具有基本的组成部分
CPU时间片: 操作系统(OS)会为每个线程分配执行时间
运行数据:
堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据。
栈空间: 存储线程需使用的局部变量,每个线程都拥有独立的栈。
线程的逻辑代码.
4.线程的特点
1. 线程抢占式执行
效率高 可防止单一线程长时间独占CPU
2.在单核CPU中,宏观上同时执行,微观上顺序执行
5.创建线程(1)--第一种方式: 继承Thread类
(上面代码块中就是第一种方式)
1.继承 Thread
2.重写run方法
3.创建线程对象
4.开启线程
获取线程ID和线程名称:
1. 在Thread的子类中调用this.getId()或this.getName()
2. 使用Thread.currentThread().getId()和 Thread.currentThread().getName()
修改线程名称:
1. 调用线程对象的setName()方法
2. 使用线程子类的构造方法赋值
示例:使用线程Thread类实现4个窗口各卖100张票
分析 一个买票线程 创建4个对象 同时循环执行100次
6.创建线程(1)--第二种方式: 实现Runnable接口
1.实现Runnable
2.重写run方法
3.创建线程对象
4.开启线程
实例:实现四个窗口共卖100张票
public class Ticket2 implements Runnable{
//实现接口
private int ticket = 100;
//重写方法
@Override
public void run() {
while (true){
if(ticket>0){
ticket--;
System.out.println(Thread.currentThread().getName()+"卖票一张,还剩下"+ticket);
}else {
break;
}
}
}
}
class TestTicket2{
public static void main(String[] args) {
//创建线程对象
Ticket2 ticket2 = new Ticket2();
Thread t1 = new Thread(ticket2,"窗口A");
Thread t2 = new Thread(ticket2,"窗口B");
Thread t3 = new Thread(ticket2,"窗口C");
Thread t4 = new Thread(ticket2,"窗口D");
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
实例:你和你女朋友公用一张银行卡,你向卡中存钱,你女朋友从卡中取钱,使用线程模拟过程!(两个线程对一个类的操作)
/**
* 存取钱 两个线程对同一进程的操作
*/
public class TestBankCard {
private static String a = new String();
public static void main(String[] args) {
BankCard bc = new BankCard(0);//卡里的初始余额是0;
SaveMoney saveMoney = new SaveMoney(bc);//存钱操作的对象
TakeMoney takeMoney = new TakeMoney(bc);//取钱操作的对象
Thread t1 = new Thread(saveMoney,"男友");
Thread t2 = new Thread(takeMoney,"女友");
t1.start();
t2.start();
}
}
/**
* 存钱
*/
class SaveMoney implements Runnable{
//引用银行卡类对象
private BankCard bankCard;
public SaveMoney(BankCard bankCard) {
this.bankCard = bankCard;
}
@Override
public void run() {
synchronized (this){
//进行10次存钱操作
for (int i = 0; i < 10; i++) {
//在原来余额的基础上加上1000,编辑为现在的余额
bankCard.setYue (bankCard.getYue()+1000);
System.out.println(Thread.currentThread().getName()+"存入1000,目前的余额是"+ bankCard.getYue());
}
}
}
}
/**
* 取钱
*/
class TakeMoney implements Runnable{
private BankCard bankCard;
public TakeMoney(BankCard bankCard) {
this.bankCard = bankCard;
}
@Override
public void run() {
synchronized (this){
//进行10次存钱操作
for (int i = 0; i < 10; i++) {
//取钱之前先判断,余额够不够用
if(bankCard.getYue()>=1000){
//在原来余额的基础上减去1000,编辑为现在的余额
bankCard.setYue (bankCard.getYue()-1000);
System.out.println(Thread.currentThread().getName()+"取出1000,目前的余额是"+ bankCard.getYue());
}else {
//如果余额不够,取钱次数减去1,并进行提醒
i--;
System.out.println("余额不足,请及时存钱");
}
}
}
}
}
/**
* 抽取一个银行卡类
*/
class BankCard{
private double yue;//余额属性
public BankCard() {
}
public BankCard(double yue) {
this.yue = yue;
}
public double getYue() {
return yue;
}
public void setYue(double yue) {
this.yue = yue;
}
@Override
public String toString() {
return "BankCard{" +
"yue=" + yue +
'}';
}
}
7.线程的常见操作方法
1.休眠:
public static void sleep(long millis)
当前线程主动休眠millis毫秒。、
/**
* Thread 休眠方法
*/
public class TestSellp1 {
public static void main(String[] args) {
T t = new T();
t.setName("wmd");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"PPPPPPPPP"+i);
}
}
}
class T extends Thread{
@Override
public void run() {
try {
//设置休眠时间并解决异常
//由于设置了休眠时间 主线程执行完后,t 才开始进行了
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"============="+i);
}
}
}
2.放弃:
public static void yield()
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片
public class TestYield {
public static void main(String[] args) {
T t1 = new T();
T t2 = new T();
t1.start();
t2.start();
}
}
class T extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
Thread.yield();
//放弃方法,抢夺下一次时间片
//这种操作增加了两个线程的交替频率
System.out.println(Thread.currentThread().getName()+"................."+i);
}
}
}
3加入:
public final void join()
允许其他线程加入到当前线程中
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t1.join();//用join方法的作用,t1循环执行完后,t2才能执行,t2执行完,t3才能执行
t2.start();
t2.join();
t3.start();
t3.join();
}
}
class T extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"........."+i);
}
}
}
4.守护线程:
线程对象.setDaemon(true);设置为守护线程。
线程有两类:用户线程(前台线程)和守护线程(后台线程)
如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。
垃圾回收线程属于守护线程
/**
* 守护线程
*/
public static void main(String[] args) {
T t = new T();
t.setDaemon(true);//设置t 为守护线程 当main 循环结束后,t的 线程任务也就结束了
//所以t的循环可能达不到20次就会结束
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("main***************"+i);
}
}
}
class T extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+".........."+i);
}
}
8.线程安全问题
需求: A线程将“Hello”存入数组;B数据将“World”存入数组。
//设置数组的初始下标
private static int index = 0;
//设置一个长度为5的数组
private static String[] arr = new String[5];
public static void main(String[] args) throws InterruptedException {
//匿名函数
Runnable hello = new Runnable() {
@Override
public void run() {
//加锁 使代码块运行时无法再分割
synchronized (arr){
//如果一个数组某个 位置是空的,name就把hello 存进去,下标+1,
//world也是同样的原理
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++;
}
}
}
};
//运行添加hello的线程
Thread t1 = new Thread(hello);
//运行添加world的线程
Thread t2 = new Thread(world);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Arrays.toString(arr));
}
synchronized (临界资源对象){//对临界资源对象加锁 //代码(原子操作) }
注意:
每个对象都有一个互斥锁标记,用来分配给线程的
只有拥有对象互斥锁标记的线程才能进入该对象加锁的同步代码块
线程退出同步代码块时候,会释放相应的互斥锁标记
经典问题:死锁
当A线程拥有锁资源a时,这时A线程需要锁资源b, 而B线程拥有锁资源b,B线程需要锁资源a, 这样会导致A等待B线程释放资源b, B线程等待A线程释放锁资源a。 从而二个处于永久等待。从而操作死锁。
public class TestLockObject {
//测试代码
public static void main(String[] args) {
Girl g = new Girl();
Boy b = new Boy();
g.setName("女生");
b.setName("男生");
g.start();
b.start();
}
}
//一个线程
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线程");
System.out.println(Thread.currentThread().getName()+"得到了两个线程");
}
}
}
}
//一个线程
class Girl 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(Thread.currentThread().getName()+"得到了两个线程");
}
}
}
}
//锁对象
class LockObject {
public static Object a = new LockObject();
public static Object b = new LockObject();
}
操作死锁得原因:
锁与锁之间有嵌套导致。
如何解决死锁:
1. 尽量减少锁得嵌套。 2. 可以使用一些安全类。 3. 可以使用Lock中得枷锁,设置枷锁时间。
9.线程通信
我们无法决定哪个线程先获取cpu,这样也就无法决定哪个线程先执行,我们如果需要指定线程的执行顺序,就需要使用线程的通信技术
简单介绍线程通信中的方法
public final void wait() 必须在对obj加锁的同步代码块中,在一个线程中,调用方法,此线程会释放期拥有的所有锁标记,同时此线程会阻塞在o的等待队列中。释放锁,进入等待队列
public final void notify() 唤醒等待队列中的线程,进入到就绪队列中,参与cpu的竞争
实例代码:
public class TestBankCard {
public static void main(String[] args) {
//创建一个银行卡对象
BankCard bc =new BankCard();
//女生银行卡操作对象是那张银行卡
GirlCard g = new GirlCard(bc);
//男生银行卡操作对象是那张银行卡
BoyCard b = new BoyCard(bc);
Thread t1 = new Thread(g,"女生");
Thread t2 = new Thread(b,"男生");
t1.start();
t2.start();
}
//一个女生类
static class GirlCard implements Runnable{
//一个银行卡属性
public BankCard bc;
public GirlCard(BankCard bc) {
this.bc = bc;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
bc.take(1000.0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//一个男友类
static class BoyCard implements Runnable {
//一个银行卡属性
private BankCard bc;
public BoyCard(BankCard bc) {
this.bc = bc;
}
@Override
public void run() {
//进行十次存钱操作
for (int i = 0; i < 10; i++) {
try {
bc.save(1000.0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//一个银行卡类
static class BankCard{
private double balance;//余额属性
private boolean flag = false;//钱的状态 有钱的时候是true 没钱的时候是false;
public BankCard(Double balance, Boolean flag) {
this.balance = balance;
this.flag = flag;
}
public BankCard() {
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public Boolean getFlag() {
return flag;
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
//一个加锁的存钱方法
public synchronized void save(Double money) throws InterruptedException {
//如果里面有钱就进入等待状态
if(flag==true){
wait();
}
//不然就进入存钱状态
this.balance=this.balance+money;
System.out.println(Thread.currentThread().getName()+"往里面存"+money+"现在里面有"+this.balance);
//改变银行卡状态
flag=true;
//唤醒等待队列状态
notify();
}
//一个加锁的取钱方法
public synchronized void take(Double money) throws InterruptedException {
//如果里面有没钱就进入等待状态
if(flag==false){
wait();
}
//不然就进入取钱钱状态
this.balance=this.balance-money;
System.out.println(Thread.currentThread().getName()+"往外面取"+money+"现在里面有"+this.balance);
//改变银行卡状态
flag=false;
//唤醒等待队列状态
notify();
}
}
}
思考:
sleep和wait得区别?
1.所在得类不同。sleep属于Thread类,wait属于Object类。 2.使用的地方: sleep可以使用再任何代码块。wait只能再同步代码块中。 3.是否释放锁资源: sleep不释放锁资源,wait会释放锁资源。 4.sleep时间到了自动唤醒,wait必须需要使用notify和notifyAll唤醒
10.线程池
什么是线程池!
该池子中预先存储若干个线程对象。整个池子就是线程池。
线程是宝贵的内存资源,单个线程约占1MB空间,过多分配易造成内存溢出,频繁的创建和销毁线程会增加虚拟机回收的频率,资源开销,造成程序性能下降。
线程池的作用:
线程容器,可以设定线程分配的数量上限
将预先创建的线程对象存入池中,并重用线程池中的先吃对象。
避免频繁的创建和销毁。
线程池的创建方式有哪些?
所有的线程池---封装了一个父接口---java.util.concurrent.Executor.
它的实现接口: ExecutorService.
有一个工具类。Executors可以帮你创建相应的线程池。
[1] 创建单一线程池 newSingleThreadExecutor()
只能创建一个线程
[2] 创建定长线程池。newFixedThreadPool(n);
可以创建长度为n的固定线程
[3] 创建可变线程池. newCachedThreadPool()
可以创建的线程的长队根据循环的次数来决定
[4] 创建延迟线程池 .newScheduledThreadPool(n);
可以设置时间单位,进行相应的延迟
public class Test1 {
public static void main(String[] args) {
//创建单一线程
//ExecutorService pool = Executors.newSingleThreadExecutor();
//创建固定线程
// ExecutorService pool = Executors.newFixedThreadPool(8);
//创建可变线程
// ExecutorService pool = Executors.newCachedThreadPool();
// for (int i = 0; i < 8; i++) {
// pool.submit(new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName()+"=============");
// }
// });
//
// }
// pool.shutdown();
//创建延迟线程
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
(pool).schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"~~~~~~~~~~~~~~~~~~");
}
},10, TimeUnit.SECONDS);
}
pool.shutdown();
}
}
11.使用最原始的方式创建线程池
上面讲解的使用Executors创建线程池的方式,都是使用底层ThreadPoolExecutor,而阿里开发手册,建议使用最原始的方式。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
public class Test02 {
public static void main(String[] args) {
/**
* int corePoolSize, 核心线程数
* int maximumPoolSize, 最大线程数
* long keepAliveTime, 空闲时间
* TimeUnit unit, 时间单位
* BlockingQueue<Runnable> workQueue: 堵塞队列,
*
* 根据你自己的业务以及服务器配置来设置。
*/
//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();
}
}
12.创建线程的第三种方式
实现Callable接口,它和实现Runnable接口差不多,只是该接口种的方法有返回值和异常抛出
应用场景,适合大文件上传
public class Test2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
My m1 = new My();
My2 m2 = new My2();
//创建一个定量长度为5的线程
ExecutorService executorService = Executors.newFixedThreadPool(5);
//分别提交线程
Future<Integer> submit = executorService.submit(m1);
Future<Integer> submit1 = executorService.submit(m2);
//拿到结果
Integer integer = submit.get();
Integer integer1 = submit1.get();
//控制台输出
System.out.println(integer+integer1);
}
//返回类型为Integer
static class My implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Integer sum = 0;
for (int i = 1; i <= 50; i++) {
sum+=i;
}
return sum;
}
}
static class My2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Integer sum = 0;
for (int i = 51; i <= 100; i++) {
sum+=i;
}
return sum;
}
}
}
13. 手动锁
Lock它是手动锁的父接口,它下面有很多实现类。
lock()方法。
unlock()释放锁资源,放在finally中
synchronized和lock有什么区别
synchronized 可以给类,方法,代码块加锁,而lock只能给代码块加锁
synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而lock需要自己加锁和释放锁,如果使用不当没有unLock() 去释放锁,就会造成死锁,
通过Lock可以知道有没有成功获取锁,而synchronized取无法办到
为保证unLock()必须会使用到
可以用finaly来操作
class TestTicket{
public static void main(String[] args) {
//四个窗口各卖100张票
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket,"窗口A");
Thread t2 = new Thread(ticket,"窗口B");
Thread t3 = new Thread(ticket,"窗口C");
Thread t4 = new Thread(ticket,"窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
public class Ticket implements Runnable {
private int ticket = 100;
//创建锁对象
Lock s = new Lock();
@Override
public void run() {
try {
//加锁
s.lock();
while(true){
if(ticket >0){
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了一张票,现在还剩下"+ticket);
}else {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
//在finally代码块中释放锁,这样释放锁的操作一定会运行
//防止中间有异常操作,导致锁无法释放
}finally {
s.unlock();
}
}
}