首页
友链
关于
留言板
推荐
电脑壁纸
在线音乐
解压游戏
Search
1
【菜谱】咖喱鸡肉盖饭
3,704 阅读
2
pycharm生成requirements.txt
1,103 阅读
3
ubuntu安装docker和docker-compose
1,018 阅读
4
celery worker启动报错:AttributeError: 'str' object has no attribute 'items'
739 阅读
5
django后台管理界面美化
638 阅读
学习
python
linux
java
菜谱
随笔
登录
Search
标签搜索
笔记
linux
python
java
实用教程
原创
编程
多线程
docker
tomcat
thread
锁
线程同步
ssm
maven
Markdown
Windows
菜谱
生活笔记
django
Dawn
累计撰写
39
篇文章
累计收到
8
条评论
首页
栏目
学习
python
linux
java
菜谱
随笔
页面
友链
关于
留言板
推荐
电脑壁纸
在线音乐
解压游戏
搜索到
39
篇与
Dawn
的结果
2021-07-27
java-生产者消费者问题以及解决办法
1.生产者消费者问题概述生产者消费者问题是多线程同步的一个经典问题。生产者消费者同时使用一块缓冲区,生产者生产商品放入缓冲区,消费者从缓冲区取出商品。我们需要保证的是,当缓冲区满时,生产者不可生产商品;当缓冲区为空时,消费者不可取出商品。这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。在生产者消费者问题中,仅有synchronized是不够的synchronized可阻止并发更新同一个共享资源,实现了同步synchronized不能用来实现不同线程之间的消息传递(通信)Java提供了几个方法解决线程之间的通信问题方法名作用wait()表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁wait(long timeout)指定等待的毫秒数notify()唤醒一个处于等待状态的线程notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStateException2.生产者消费者问题的解决办法2.1 解决思路采用某种机制保护生产者消费者之间的同步,有较高的效率,并且易于实现,代码的可控制性较好,属于常用的方式在生产者和消费者之间建立一个管道,管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。解决核心在于保证同一资源被多个线程并发访问时的完整性,常用的同步方法时采用信号或者加锁机制,保证资源在任意时刻至多被一个线程访问2.2 实现方法wait()和nofity()方法await()和signal()方法BlockingQueue阻塞队列方法管道法信号量2.3 代码实现2.3.1 wait()和nofity()方法当缓冲区已满时,生产者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行;当缓冲区已空时,消费者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行。当生产者向缓冲区放入一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态;当消费者从缓冲区取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。示例://测试生产者消费者模型--利用缓冲区解决 public class TestProductAndConsume1 { public static void main(String[] args) { SynContainer synContainer = new SynContainer(); new Product(synContainer).start(); new Consumer(synContainer).start(); } } //生产者 class Product extends Thread { SynContainer container; public Product(SynContainer container){ this.container = container; } @Override public void run() { for (int i = 0; i < 100; i++) { container.push(new Chicken(i)); System.out.println("生产了第" + i + "只鸡"); } } } //消费者 class Consumer extends Thread { SynContainer container; public Consumer(SynContainer container){ this.container = container; } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("消费了" + container.pop().id + "只鸡"); } } } //产品 class Chicken { int id; public Chicken(int id) { this.id = id; } } //缓冲区 class SynContainer { //需要一个容器大小 Chicken[] chickens = new Chicken[10]; //容器计数器 int count = 0; //生产者放入产品 public synchronized void push(Chicken chicken){ //容器满了,等待消费者消费 if (count == chickens.length){ //通知消费者消费,生产者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果容器没有满,放入产品 chickens[count] = chicken; count++; //通知消费者消费 this.notifyAll(); } //消费者消费产品 public synchronized Chicken pop(){ //判断能否消费 if (count == 0){ //等待生产者生产,消费者消费 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果可以消费 count--; Chicken chicken = chickens[count]; //吃完了,通知生产者生产 this.notifyAll(); return chicken; } }notify()方法可使所有正在等待队列中等待同一共享资源的"全部"线程从等待状态退出,进入可运行状态。此时,优先级最高的哪个线程最先执行,但也有可能是随机执行的,这要取决于JVM虚拟机的实现。即最终也只有一个线程能被运行,上述线程优先级都相同,每次运行的线程都不确定是哪个,后来给线程设置优先级后也跟预期不一样,主要看JVM具体实现。2.3.2 await()/signal()方法在JDK5中,用ReenteantLock和Condition可以实现等待/通知模型,具有更大的灵活性。通过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全。示例:(在这里只需要改动缓冲区即仓库类)//缓冲区 class SynContainer { //需要一个容器大小 Chicken[] chickens = new Chicken[10]; //容器计数器 int count = 0; // 锁 private final Lock lock = new ReentrantLock(); // 仓库满的条件变量 private final Condition full = lock.newCondition(); // 仓库空的条件变量 private final Condition empty = lock.newCondition(); //生产者放入产品 public void push(Chicken chicken){ //获得锁 lock.lock(); try { //容器满了,等待消费者消费 if (count == chickens.length) { //通知消费者消费,生产者等待 try { full.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果容器没有满,放入产品 chickens[count] = chicken; count++; //通知消费者消费 empty.signalAll(); } finally { lock.unlock();//释放锁 } } //消费者消费产品 public Chicken pop(){ //获得锁 lock.lock(); Chicken chicken = null; try { //判断能否消费 if (count == 0) { //等待生产者生产,消费者消费 try { empty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果可以消费 count--; chicken = chickens[count]; //吃完了,通知生产者生产 full.signalAll(); } finally { lock.unlock(); } return chicken; } }2.3.3 BlockingQueue阻塞队列方法BlockingQueue是JDk5新增内容,它是一个已经在内部实现了同步的队列,实现方式采用的是我们第2种await()/signal()方法。它可以在生成对象时指定容量大小,用于阻塞操作的是put()和take()方法。put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。示例:(在这里只需要改动缓冲区即仓库类)//缓冲区 class SynContainer { // 仓库存储的载体 private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<>(10); //生产者放入产品 public void push(Chicken chicken) { try { list.put(chicken); } catch (InterruptedException e) { e.printStackTrace(); } } //消费者消费产品 public Chicken pop() { Chicken chicken = null; try { chicken = (Chicken) list.take(); } catch (InterruptedException e) { e.printStackTrace(); } return chicken; } }可能会出现put()或take()和System.out.println()输出不匹配的情况,是由于它们之间没有同步造成的。BlockingQueue可以放心使用,这可不是它的问题,只是在它和别的对象之间的同步有问题。2.3.4 Semaphore信号量Semaphonre是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可对象,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,比如数据库 连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。计数为0的Semphore是可以release的,然后就可以acquire(即一开始使线程阻塞从而完成其他执行)。示例:(在这里只需要改动缓冲区即仓库类)//缓冲区 class SynContainer { // 仓库存储的载体 private LinkedList<Object> list = new LinkedList<Object>(); // 仓库的最大容量 final Semaphore notFull = new Semaphore(10); // 将线程挂起,等待其他来触发 final Semaphore notEmpty = new Semaphore(0); // 互斥锁 final Semaphore mutex = new Semaphore(1); //生产者放入产品 public void push(Chicken chicken) { try { notFull.acquire(); mutex.acquire(); list.add(chicken); } catch (InterruptedException e) { e.printStackTrace(); }finally { mutex.release(); notFull.release(); } } //消费者消费产品 public Chicken pop() { Chicken chicken = null; try { notFull.acquire(); mutex.acquire(); chicken = (Chicken) list.remove(); } catch (InterruptedException e) { e.printStackTrace(); }finally { mutex.release(); notFull.release(); } return chicken; } }2.3.5 管道管道是一种特殊的流,用于不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道读数据。inputStream.connect(outputStream)或outputStream.connect(inputStream)作用是使两个Stream之间产生通信链接,这样才可以将数据进行输入和输出。这种方式只适用于两个线程之间通信,不适合多个线程之间通信。2.4.5.1 PipedInoutStream/PipedOutputStream(操作字节流)示例://测试生产者消费者问题--管道-PipedInoutStream/PipedOutputStream(操作字节流) public class TestProductAndConsume5 { public static void main(String[] args) { Producer producer = new Producer(); Consumer consumer = new Consumer(); Thread thread1 = new Thread(producer); Thread thread2 = new Thread(consumer); try { producer.getPipedOutputStream().connect(consumer.getPipedInputStream()); thread2.start(); thread1.start(); } catch (IOException e) { e.printStackTrace(); } } } class Producer implements Runnable { private PipedOutputStream pipedOutputStream; public Producer() { pipedOutputStream = new PipedOutputStream(); } public PipedOutputStream getPipedOutputStream() { return pipedOutputStream; } @SneakyThrows @Override public void run() { try { for (int i = 0; i < 5; i++) { pipedOutputStream.write(("This is a test , ID = " + i + "!\n").getBytes()); } pipedOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } class Consumer implements Runnable { private PipedInputStream pipedInputStream; public Consumer() { pipedInputStream = new PipedInputStream(); } public PipedInputStream getPipedInputStream() { return pipedInputStream; } @SneakyThrows @Override public void run() { int length = -1; byte[] buffer = new byte[1024]; try { while ((length = pipedInputStream.read(buffer)) != -1) { System.out.println(new String(buffer, 0, length)); } pipedInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }2.4.5.2 PipedReader/PipedWriter(操作字符流)示例与操作字节流基本一致,只是替换相应流,这里不再贴代码
2021年07月27日
139 阅读
0 评论
0 点赞
2021-07-27
java-Lock锁概述
1.Lock锁概述从JDK5.0开始,Java提供了更强大的线程同步机制--通过显式定义同步锁对象来实现同步。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的时ReentrantLock,可以显式加锁、释放锁。Lock是一个接口,两个直接实现类:ReentrantLock(可重入锁),ReentrantReadWriteLock(读写锁)语法://定义锁 private final ReentrantLock lock = new ReentrantLock(); lock.lock();//加锁 try { //保证线程安全的代码 }finally { lock.unlock;//解锁 }示例:public class TestLock{ public static void main(String[] args) { TestLock2 testLock2 = new TestLock2(); new Thread(testLock2).start(); new Thread(testLock2).start(); new Thread(testLock2).start(); } } class TestLock2 implements Runnable{ int ticketNums = 10; //定义lock锁 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { lock.lock();//加锁 try{ if (ticketNums > 0){ try { Thread.sleep(1000); System.out.println(ticketNums--); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } }finally { lock.unlock();//解锁 } } } }2. Lock锁的API修饰符和类型方法描述voidlock()获得锁voidlockInterruptibly()获得锁,可中断。举个例子,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。booleantryLock()锁在空闲的时候才能获取锁(未获得锁不会等待)。举个例子:当两个线程同时通过lock.terLock()想获取某个锁时,假若此时线程A获取到了锁,而线程B不会等待,直接放弃获取锁。booleantryLock(Long time, TimeUnit unit)如果锁定可用,则此方法立即返回true如果锁不可用,则当前线程将被禁用以进行线程调度,并且在发生以下三种情况之一之前处于休眠状态: 1.当前线程获取锁 2.其它一些线程中断当前线程 3.等待时间过去了,返回falsevoidunlock()释放锁3.synchronized与Lock的对比Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放Lock只有代码块锁,synchronized有代码块锁和方法锁使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了资源) > 同步方法(在方法体之外)
2021年07月27日
123 阅读
0 评论
0 点赞
2021-07-27
java死锁简介
1.死锁的定义死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。即某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生"死锁"的问题。示例:public class TestDeadLock { public static void main(String[] args) { MakeUp makeUp1 = new MakeUp(0, "小灰"); MakeUp makeUp2 = new MakeUp(1, "小白"); makeUp1.start(); makeUp2.start(); } } class Lipstick{ } class Mirror{ } class MakeUp extends Thread { //需要的资源只有一份 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() { //化妆 makeUp(); } private void makeUp(){ if (choice == 0){ synchronized (lipstick){//获得口红的锁 try { System.out.println(this.girlName + "获得口红的锁"); Thread.sleep(1000); synchronized (mirror){//1秒钟后获得镜子的锁 System.out.println(this.girlName + "获得镜子的锁"); } } catch (InterruptedException e) { e.printStackTrace(); } } // synchronized (mirror){//1秒钟后获得镜子的锁 // System.out.println(this.girlName + "获得镜子的锁"); // } }else{ synchronized (mirror){//获得镜子的锁 try { System.out.println(this.girlName + "获得镜子的锁"); Thread.sleep(2000); synchronized (lipstick){//1秒钟后获得口红的锁 System.out.println(this.girlName + "获得口红的锁"); } } catch (InterruptedException e) { e.printStackTrace(); } } // synchronized (lipstick){//1秒钟后获得口红的锁 // System.out.println(this.girlName + "获得口红的锁"); // } } } }2.产生死锁的四个必要条件互斥条件:一个资源每次只能被一个进程使用。请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。上面列出了死锁的四个必要条件,我们只要想办法破其中任意一个或多个条件就可以避免死锁发生。
2021年07月27日
67 阅读
0 评论
0 点赞
2021-07-22
线程同步及synchronized关键字实现同步的方法
1.什么叫线程同步?多个线程操作同一个资源即并发,同一个对象被多个线程同时操作(抢票等)。线程同步是为了确保线程安全,所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。如果多线程程序运行结果和单线程运行的结果是一样的,且相关变量的值与预期值一样,则是线程安全的。处理多线程问题时,最天然的解决办法就是'排队',线程同步就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。线程同步形成条件:队列+锁但由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问的冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程或的对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,但会存在以下几个问题:一个线程持有锁会导致其它所有需要此锁的线程挂起在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,从而引起性能问题如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,出现性能倒置问题## 2.实现同步机制的方法我们都知道可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized块和synchronized方法。2.1 同步代码块语法:synchronzied(obj){}obj称之为同步监视器obj可以是任何对象,但是推荐使用共享资源作为同步监视器同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class同步监视器的执行过程第一个线程访问,锁定同步监视器,执行其中代码第二个线程访问,发现同步监视器被锁定,无法访问第一个线程访问完毕,解锁同步监视器第二个线程访问,发现同步监视器没有锁,然后锁定并访问示例:public class TestUnsafeBank { public static void main(String[] args) { Account account = new Account("结婚基金", 500); Drawing drawing1 = new Drawing(account, 300, "男朋友"); Drawing drawing2 = new Drawing(account, 400, "女朋友"); drawing1.start(); drawing2.start(); } } class Account{ private String name; private int money; public Account(String name, int money) { this.name = name; this.money = money; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } } class Drawing extends Thread{ private Account account; private int drawingMoney;//取钱 private int haveMoney;//手里有的钱 public Drawing(Account account, int drawingMoney, String name){ super(name); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { //锁得对象是变化的对象 synchronized (account){ //同步代码块,同步监视器是银行账户 if (drawingMoney > account.getMoney()){ System.out.println(this.getName() + "取钱时账户钱不够"); return; } //模拟延时 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.setMoney(account.getMoney() - drawingMoney); haveMoney = haveMoney + drawingMoney; System.out.println(this.getName() + "手里现在有:" + haveMoney); System.out.println("账户余额为:" + account.getMoney()); } } }2.2 同步方法synchronized方法控制对对象的的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。若将一个大的方法声明为synchronized将会影响效率方法里面需要修改的内容才需要锁,锁的太多,浪费资源语法:public synchronized void method(){}示例:public class TestUnsafeBuyTicket { public static void main(String[] args) { BuyTicket buyTicket = new BuyTicket(); new Thread(buyTicket, "学生").start(); new Thread(buyTicket, "打工人").start(); new Thread(buyTicket, "黄牛党").start(); } } class BuyTicket implements Runnable{ private int ticketNums = 10; boolean flag = true; //外部停止方法 @Override public void run() { //买票 while (flag) { buy(); } } private synchronized void buy(){ //同步方法 //判断是否有票 if (ticketNums <= 0){ flag = false; return; } //模拟延时 try { Thread.sleep(100); //买票 System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--); } catch (InterruptedException e) { e.printStackTrace(); } } }
2021年07月22日
93 阅读
0 评论
0 点赞
2021-07-22
线程的常用方法梳理
1.线程的五大状态创建状态就绪状态阻塞状态运行状态死亡状态2.线程停止不推荐使用jdk提供的stop()、destory()方法。【已废弃】推荐线程自己停止下来建议使用一个标识位进行终止变量示例:/* 测试停止 * 1.建议线程正常停止 * 2.建议使用标识位 * 3.不要使用stop和destroy等过时或者jdk不建议使用的方法 */ public class TestStop implements Runnable{ //1.设置一个标识位 private boolean flag = true; @Override public void run() { int i = 0; while (flag) { System.out.println("线程开始运行" + i++); } } //2.设置一个方法转换标识位 public void stop(){ this.flag = false; } public static void main(String[] args) { TestStop testStop = new TestStop(); new Thread(testStop).start(); for (int i = 0; i < 1000; i++) { System.out.println("主线程开始" + i); if (i == 900) { //调用stop方法切换标识位,使线程停止 testStop.stop(); System.out.println("线程停止了"); } } } }3. 线程休眠_sleepsleep时间指定为当前线程阻塞的毫秒数sleep存在InterruptedException异常sleep时间到时线程进入就绪状态每一个对象都有一把锁,sleep不会释放锁示例1:模拟网络延时public class TestThread2 extends Thread { private int ticketNums=10; @Override public void run() { //重写run()方法 while (true){ if (ticketNums<=0){ break; } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getName()+"拿到了第"+ticketNums--+"票"); } } public static void main(String[] args) { //创建Thread子类的实例 TestThread2 testThread = new TestThread2(); //调用start()方法开启线程 testThread.start(); } }示例2:模拟倒计时public class TestSleep { public static void main(String[] args) throws InterruptedException { TestSleep testSleep = new TestSleep(); testSleep.tenDown(); //打印系统时间 Date startTime = new Date(System.currentTimeMillis()); while (true){ try { Thread.sleep(1000); System.out.println(new SimpleDateFormat("yy-MM-dd HH:mm:ss").format(startTime)); startTime = new Date(System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } //模拟倒计时 public void tenDown() throws InterruptedException { int num = 10; while(true){ Thread.sleep(1000); System.out.println(num--); if (num <= 0) { break; } } } }4.线程礼让_yield礼让线程,让当前正在执行的线程暂停,但不阻塞将线程从运行状态转为就绪状态让cpu重新调度,礼让不一定成功示例://礼让不一定成功,看cpu调度 public class TestYield { public static void main(String[] args) { MyYield myYield = new MyYield(); new Thread(myYield, "a").start(); new Thread(myYield, "b").start(); } } class MyYield implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程开始执行"); Thread.yield();//礼让 System.out.println(Thread.currentThread().getName() + "线程停止执行"); } }5.线程强制执行_joinjoin合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞可以想象成插队示例://测试join方法(插队) public class TestJoin implements Runnable { public static void main(String[] args) throws InterruptedException { //启动VIP TestJoin testJoin = new TestJoin(); Thread thread = new Thread(testJoin); thread.start(); //main线程 for (int i = 0; i < 1000; i++) { if (i == 200){ thread.join(); } System.out.println("main" + i); } } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("vip来了"); } } }6.获取线程状态_getState线程状态,线程可以处于以下状态之一:NEW 尚未启动的线程处于此状态(创建状态)RUNNABLE 在Java虚拟机中执行的线程处于此状态(运行状态)BLOCKED 被阻塞等待监视器锁定的线程处于此状态(阻塞状态)WAITING 正在等待另一个线程执行特定动作的线程处于此状态(阻塞状态)TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态(阻塞状态)TERMINATED 已退出的线程处于此状态(死亡状态)一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统线程状态的的虚拟机状态示例:public class TestState { public static void main(String[] args) { Thread thread = new Thread(() -> { for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("========"); }); //观察状态 Thread.State state = thread.getState(); System.out.println(state); //new thread.start(); state = thread.getState(); System.out.println(state); while (state != Thread.State.TERMINATED){ //只要线程不停止,就一直运行 try { Thread.sleep(100); state = thread.getState(); System.out.println(state); } catch (InterruptedException e) { e.printStackTrace(); } } } }7.线程的优先级_setPriorityjava提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。线程的优先级用数字表示,范围从1-10Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10Thread.NORM_PRIORITY = 5使用getPriority来获取优先级,setPriority来改变优先级优先级高并不一定优先执行,线程的执行顺序真正取决于CPU调度器示例://测试优先级 public class TestPriority{ public static void main(String[] args) { //主线程默认优先级 System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority()); MyPriority myPriority = new MyPriority(); Thread t1 = new Thread(myPriority); Thread t2 = new Thread(myPriority); Thread t3 = new Thread(myPriority); Thread t4 = new Thread(myPriority); //先设置优先级,再启动 t1.start(); t2.setPriority(1); t2.start(); t3.setPriority(4); t3.start(); t4.setPriority(Thread.MAX_PRIORITY); t4.start(); } } class MyPriority extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority()); } }8. 设置守护线程_setDaemon线程分为用户线程和守护线程虚拟机必须确保用户线程执行完毕虚拟机不用等待守护线程如,后台记录操作日志,监控日志,垃圾回收等示例://测试守护线程 public class TestDaemon { public static void main(String[] args) { God god = new God(); You you = new You(); Thread thread = new Thread(god); thread.setDaemon(true); //默认是false,表示是用户线程 thread.start();//守护线程启动 new Thread(you).start();//用户线程启动 } } //上帝 class God implements Runnable { @Override public void run() { while (true) { System.out.println("上帝保佑你"); } } } //你 class You implements Runnable { @Override public void run() { for (int i = 0; i < 36500; i++) { System.out.println("开心的活了100多岁"); } } }
2021年07月22日
175 阅读
0 评论
0 点赞
2021-07-22
java8新特性之Lambda简介及简单用法介绍
1. Lambda简介Lambda表达式是JDK8的一个新特性,可以取代大部分的匿名内部类,写出更优雅的Java代码,尤其在集合遍历和其它集合操作中,可以极大地优化代码结构。1.1 对接口的要求Lambda规定接口中只能有一个需要被实现的方法,不是规定接口中只有一个方法JDK8中有另外一个新特性:default,被default修饰的方法会有默认实现,不是必须被实现的方法,所以不影响Lambda表达式的使用。1.2 什么是函数式接口在Java中,标记类型的接口是一种没有方法或属性声明的接口,简单地说,标记接口是空接口,相似地,函数式接口是只包含一个抽象方法声明的接口。@FunctionalInterface是Java8新加入的一种接口,用于注明该接口类型声明是根据Java语言规范定义的函数式接口。Java8还声明了一些Lambda表达式可以使用的函数式接口,当你注释的接口不是有效的函数式接口时,可以使用@FunctionalInterface解决编译层面的错误。2. Lambda结构一个Lambda表达式可以有零个或多个参数参数的类型既可以明确声明,也可以根据上下文来推断。例如(String s)与(s)相同所有参数需包含在圆括号内,参数之间用逗号相隔。例如(a, b)或(int a, int b)空圆括号代表参数集为空当只有一个参数,且其类型可推导时,圆括号可省略Lambda表达式的主体可包含零条或多条语句若Lambda表达式的主体只有一条语句花括号{}可省略。匿名函数的返回类型与该主体表达式相同若Lambda表达式的主体包含一条以上语句,则表达式必须包含在花括号{}之中,匿名函数的返回类型与该代码块返回值相同3. Lambda表达式基础语法我们先定义6个接口类,后面的操作实例均在此基础上进行。//多参数无返回 @FunctionalInterface public interface NoReturnMultiParam { void method(int a, int b); } //无参无返回值 @FunctionalInterface public interface NoReturnNoParam { void method(); } //一个参数无返回 @FunctionalInterface public interface NoReturnOneParam { void method(int a); } //多个参数有返回值 @FunctionalInterface public interface ReturnMultiParam { int method(int a, int b); } //无参有返回 @FunctionalInterface public interface ReturnNoParam { int method(); } //一个参数有返回值*/ @FunctionalInterface public interface ReturnOneParam { int method(int a); }语法形式为() -> {}()用来描述参数列表{}用来描述方法体->为lambda运算符,读作(goes to)示例:public class LambdaTest { public static void main(String[] args) { //无参无返回 NoReturnNoParam noReturnNoParam = () -> { System.out.println("无参无返回"); }; noReturnNoParam.method(); //一个参数无返回 NoReturnOneParam noReturnOneParam = (int a) -> { System.out.println("一个参数无返回:" + a); }; /**简化版 NoReturnOneParam noReturnOneParam = a -> { System.out.println("一个参数无返回:" + a); }; */ noReturnOneParam.method(666); //多个参数无返回 NoReturnMultiParam noReturnMultiParam = (int a, int b) -> { System.out.println("多个参数无返回:{" + a + "," + b + "}"); }; /**简化版 NoReturnMultiParam noReturnMultiParam = (a, b) -> { System.out.println("多个参数无返回:{" + a + "," + b + "}"); }; */ noReturnMultiParam.method(1, 1); //无参有返回 ReturnNoParam returnNoParam = () -> { System.out.println("无参有返回"); return 1; }; int res = returnNoParam.method(); System.out.println("无参有返回的返回值:" + res); //一个参数有返回值 ReturnOneParam returnOneParam = (int a) -> { System.out.println("一个参数有返回值:" + a); return 1; }; /**简化版 ReturnOneParam returnOneParam = a -> { System.out.println("一个参数有返回值:" + a); return 1; }; */ res = returnOneParam.method(666); System.out.println("一个参数有返回值的返回值:" + res); //多个参数有返回值 ReturnMultiParam returnMultiParam = (int a, int b) -> { System.out.println("多个参数有返回值:{" + a + "," + b + "}"); return 1; }; /**简化版 ReturnMultiParam returnMultiParam = (a, b) -> { System.out.println("多个参数有返回值:{" + a + "," + b + "}"); return 1; }; */ res = returnMultiParam.method(1, 2); System.out.println("多个参数有返回值的返回值:" + res); ReturnMultiParam lambda1 = (a, b) -> a + b; ReturnMultiParam lambda2 = Integer::sum; System.out.println(lambda1.method(1, 1)); System.out.println(lambda2.method(2, 2)); } }4.Lambda表达式常用示例4.1 Lambda表达式引用方法有时我们不是必须要自己重写某个匿名内部类的方法,我们可以利用Lambda表达式的接口快速指向一个已经被实现的方法。语法:方法归属者::方法名 静态方法的归属者为类名,普通方法的归属者为对象public class LambdaTest2 { public static void main(String[] args) { ReturnOneParam lambda3 = a -> doubleNum(a); System.out.println(lambda3.method(2)); //lambda4 引用了已经实现的 doubleNum 方法 ReturnOneParam lambda4 = LambdaTest::doubleNum; System.out.println(lambda4.method(2)); LambdaTest lambdaTest = new LambdaTest(); //lambda5 引用了已经实现的 addTwo 方法 ReturnOneParam lambda5 = lambdaTest::addTwo; System.out.println(lambda5.method(3)); } /** * 要求 * 1.参数数量和类型要与接口中定义的一致 * 2.返回值类型要与接口中定义的一致 */ public static int doubleNum(int a) { return a * 2; } public int addTwo(int a) { return a + 2; } } 4.2 构造方法的引用一般我们需要声明接口,该接口作为对象的生成器,通过类名::new 的方式来实例化对象,然后调用方法返回对象。先创建个实体类供之后使用:public class Item { private int id; private String name; private int price; public Item() { } public Item(int id, String name, int price) { this.id = id; this.name = name; this.price = price; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } }示例:public class LambdaTest3 { interface ItemCreatorBlankConstruct { Item getItem(); } interface ItemCreatorParamConstruct { Item getItem(int id, String name, int price); } public static void main(String[] args) { ItemCreatorBlankConstruct construct = () -> new Item(); Item item = construct.getItem(); ItemCreatorBlankConstruct construct2 = Item::new; Item item2 = construct2.getItem(); ItemCreatorParamConstruct construct3 = Item::new; Item item3 = construct3.getItem(11, "macbook pro", 8999); } }4.3 Lambda表达式创建线程以往我们创建线程都是通过创建Thread对象,然后通过匿名内部类重写run()方法,到这里看到匿名内部类就应该可以想到使用Lambda表达式来简化线程创建过程。//旧方法: new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程:" + i); } } }).start(); //新方法 new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("线程:" + i); } }).start;4.4 遍历集合我们可以调用集合的public void forEach(Consumer<? super E> action)方法,通过Lambda表达式的方式遍历集合中的元素,Consumer 接口是 JDK 为我们提供的一个函数式接口。ArrayList<Integer> list = new ArrayList<>(); Collections.addAll(list, 1,2,3,4,5); //旧方法 for(Integer n: list) { System.out.println(n); } //新方法 list.forEach(n -> System.out.println(n)); //或者使用引用 list.forEach(System.out::println);4.5 删除集合中的某个元素我们通过public boolean removeIf(Predicate<? super E> filter)方法来删除集合中的某个元素,Predicate 也是JDK 为我们提供的一个函数式接口,可以简化程序的编写。ArrayList<Item> items = new ArrayList<>(); items.add(new Item(3, "牙刷", 1)); items.add(new Item(2, "牙膏", 3)); items.add(new Item(1, "牙缸", 5)); items.add(new Item(4, "假牙", 2000)); items.add(new Item(5, "冲牙器", 100)); //旧方法 for (Item item : items) { if (item.getId() == 4){ items.remove(item); } } //新方法 items.removeIf(ele -> ele.getId() == 4); items.forEach(System.out::println);4.6 集合内元素的排序在以前我们若要为集合内的元素排序,就必须调用 sort 方法,传入比较器匿名内部类重写 compare 方法,我们现在可以使用 lambda 表达式来简化代码。//旧方法 items.sort(new Comparator<Item>() { @Override public int compare(Item o1, Item o2) { return o1.getId() - o2.getId(); } }); //新方法 items.sort((o1,o2) -> o1.getId() - o2.getId()); items.forEach(System.out::println);4.7 Lambda表达式中的闭包问题这个问题我们在匿名内部类中也会存在,如果我们把注释放开就会报错,告诉我们num值是final不能被改变。这里我们虽然没有标识 num 类型为 final,但是在编译期间虚拟机会帮我们加上 final 修饰关键字。int num = 10; Consumer<String> consumer = ele -> { System.out.println(num); }; // num = num + 2; consumer.accept("hello");
2021年07月22日
66 阅读
0 评论
0 点赞
2021-07-13
线程创建的三种方式及区别
一. 创建方式1. 继承Thread类定义Thread类的子类,并重写该类的run方法。创建Thread子类的实例,即创建了线程对象。调用线程对象的start()方法来启动线程。package Thread; /* 1.继承Thread类 2.重写run()方法 3.调用start()方法开启线程 */ public class TestThread extends Thread { @Override public void run() { //重写run()方法 for (int i = 0; i < 10; i++) { System.out.println("小明在厨房做饭"); } } public static void main(String[] args) { //创建Thread子类的实例 TestThread testThread = new TestThread(); //调用start()方法开启线程 testThread.start(); for (int i = 0; i < 200; i++) { System.out.println("小红在客厅打王者"); } } }当多线程没有开启时,代码只执行主线程里面main()方法的代码,当开启线程后,经过cpu调度后开始执行(线程开启后不一定立即执行)运行结果:2. 实现Runnable接口实现Runnable接口,并重写该接口的run()方法。创建Runnable实现类的实例,并用该实例作为Thread的target来创建Thread对象,Thread对象才是真正的线程对象。调用线程对象的start()方法来启动线程。package Thread; /* 1.实现Runnable接口 2.重写run()方法 3.创建Runnable接口实现类实例,并将该实例放入Thread的target来创建Thread对象 4.调用start()方法开启线程 */ public class TestRunnable implements Runnable { @Override public void run() { //重写run()方法 for (int i = 0; i < 10; i++) { System.out.println("小明在厨房做饭"); } } public static void main(String[] args) { //创建Runnable接口实现类实例 TestRunnable testRunnable = new TestRunnable(); //该实例放入Thread的target来创建Thread对象 //调用start()方法开启线程 // Thread thread = new Thread(testRunnable); // thread.start(); new Thread(testRunnable).start(); for (int i = 0; i < 200; i++) { System.out.println("小红在客厅打王者"); } } }执行结果:3. 实现Callable接口实现Callable接口并指定返回值,然后实现call()方法。创建Callable实现类的实例,使用FutureTask包装类来包装Callable对象。使用FutureTask对象作为Thread对象的target创建并启动新线程。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。package Thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /* 1.实现Callable接口。 2.重写call()方法。 3.创建Callable接口实现类实例,使用FutureTask包装类来包装Callable实例对象。 4.使用FutureTask对象作为Thread的target来创建Thread对象。 5.调用start()方法开启线程。 6.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。 */ public class TestCallable implements Callable<Boolean> { @Override public Boolean call() throws Exception { //重写call()方法 for (int i = 0; i < 10; i++) { System.out.println("小明在厨房做饭"); } return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { //创建Callable接口实现类实例 TestCallable testCallable = new TestCallable(); //使用FutureTask包装类来包装Callable实例对象 FutureTask<Boolean> task = new FutureTask<>(testCallable); //使用FutureTask对象作为Thread的target来创建Thread对象 //调用start()方法开启线程 new Thread(task).start(); for (int i = 0; i < 200; i++) { System.out.println("小红在客厅打王者"); } //调用FutureTask对象的get()方法来获得子线程执行结束后的返回值 System.out.println(task.get()); //获取call()的返回值。若调用时call()方法未返回,则会阻塞线程等待返回值 } }执行结果:二. 创建线程三种方式的优缺点#### 1. 继承Thread类优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可。缺点:java是单继承的,故无法再继承其他类。#### 2. 实现Runnable接口优点:线程类只是实现了Runnable接口,还可以继承其他类。这种情况下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将cpu代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。#### 3. Runnable和Callable的区别Runnable规定的方法是run(),Callable规定的是call()。Runnable的任务执行后无返回值,Callable的任务执行后有返回值。call()方法可以抛出异常,run()方法不可以,因为run()方法本身没有抛出异常,所以自定义的线程类在重写run()方法的时候也无法抛出异常。运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消人物的执行,还可获取执行结果。#### 4. start()和run()的区别start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行。run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)。
2021年07月13日
158 阅读
0 评论
0 点赞
2021-02-26
ubuntu操作系统使用docker容器搭建typecho博客详细教程
1.安装docker和docker-compose这一步不再赘述,详情参考博客文章 ubuntu安装docker和docker-compose2.需要的镜像今天笔者这里使用以下三个镜像:nginxdawn/php:7.2.3-fpmmysql:5.7(PS:笔者这样做是为了留出单独的php和nginx环境以便后续使用,如果直接使用typecho镜像会更加简单,有兴趣的可以研究一下, 这里不做赘述!)其中nginx和mysql:5.7为官方镜像,dawn/php:7.2.3-fpm是在官方的基础上添加了pdo_mysql(这样可以使用mysql作为博客的数据库)dawn/php:7.2.3-fpm的Dockerfile如下:FROM php:7.2.3-fpm LABEL maintainer="dawn" \ email="****@qq.com" \ version="7.2.3" RUN apt-get update \ && docker-php-ext-install pdo_mysql \ && echo "output_buffering = 4096" > /usr/local/etc/php/conf.d/php.ini3.配置新建blog文件夹,其目录结构如下:|--mysql //mysql持久化目录 | └──conf //mysql配置目录 | |--data //mysql数据库目录 | |--db //单独建的存放sql文件的目录 | |--logs //mysql日志目录 |--nginx //nginx 配置文件的持久化目录 | └──conf //nginx配置目录 | └──nginx.conf //nginx全局配置文件 | |--blog.conf //博客站点配置文件 | |--logs //nginx日志目录 |--ssl //SSL证书目录 |--www //php站点根目录 |--tomcat //tomcat持久化目录(博客用不到,不展开叙述) |--docker-compose.yml //docker-compose 配置文件 |--Dockerfile //Dockerfile文件 |--mysql.env //mysql配置信息3.1 配置docker-compose.ymlversion: '3' services: nginx: image: nginx:latest container_name: nginx ports: - "80:80" - "443:443" volumes: - ./www/:/var/www/html - ./ssl/:/var/www/ssl - ./nginx/blog.conf:/etc/nginx/conf.d/blog.conf - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/logs:/var/log/nginx restart: always depends_on: - fpm - mysql environment: - TZ=Asia/Shanghai fpm: build: ./ image: dawn/php:7.2.3-fpm container_name: php_fpm ports: - "9000:9000" volumes: - ./www/:/var/www/html restart: always depends_on: - mysql environment: - TZ=Asia/Shanghai mysql: image: mysql:5.7 container_name: mysql ports: - "3306:3306" volumes: - ./mysql/data:/var/lib/mysql - ./mysql/logs:/var/log/mysql - ./mysql/conf:/etc/mysql/conf.d restart: always env_file: - mysql.env其中:version 指定docker-compose版本image 指定镜像名称build 指定Dockerfile上下文目录ports 指定端口映射container_name 指定容器名字volumes 指定文件挂载映射depends_on 指定服务启动时的先后顺序,指定的服务会先于当前服务启动3.2 配置nginx全局配置文件nginx.conf,参考内容如下:user www-data; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; }站点配置目录blog.conf,参考内容如下:server { listen 80; server_name dawnsite.cn www.dawnsite.cn; # 更换为你的站点域名 # http重定向到https return 301 https://$host$request_uri; } server { listen 443; server_name dawnsite.cn www.dawnsite.cn; # 更换为你的站点域名 root /var/www/html/build; # 有的为/var/www/html,这里视项目情况更改 index index.php index.html index.htm; ssl on; ssl_certificate /var/www/ssl/4884437_dawnsite.cn.pem; # 更换为你的站点证书 ssl_certificate_key /var/www/ssl/4884437_dawnsite.cn.key; # 更换为你申请的私钥 ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; # 伪静态规则 if (-f $request_filename/index.html){ rewrite (.*) $1/index.html break; } if (-f $request_filename/index.php){ rewrite (.*) $1/index.php; } if (!-f $request_filename){ rewrite (.*) /index.php; } # 非www开头->www开头 更换为你的站点域名 if ($host ~ '^dawnsite.cn'){ return 301 https://www.dawnsite.cn$request_uri; } location ~ \.php$ { fastcgi_pass fpm:9000; fastcgi_index index.php; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } 3.3 配置mysqlmysql配置信息mysql.env,参考内容如下:# MySQL的root用户默认密码,这里自行更改 MYSQL_ROOT_PASSWORD=password # MySQL镜像创建时自动创建的数据库名称 MYSQL_DATABASE=typecho # MySQL镜像创建时自动创建的用户名 MYSQL_USER=user # MySQL镜像创建时自动创建的用户密码 MYSQL_PASSWORD=password # 时区 TZ=Asia/Shanghai4.安装4.1 编排容器在blog目录下执行以下命令:docker-compose up -d查看进程docker-compose ps4.2 安装typecho4.2.1 下载typecho容器编排成功后,进入blog目录下的www目录开始下载typecho操作wget http://typecho.org/downloads/1.1-17.10.30-release.tar.gz解压tar -xzvf 1.1-17.10.30-release.tar.gz解压后所有文件会在一个build文件夹内,这也就是我为什么在blog.conf中那样配置root的原因,当然你也可以把他从build移出来放到www目录下,这样blog.conf就会变成大多数人说的那样啦4.2.2 配置typecho浏览器输入域名URL,进入typecho安装页面PS:数据库地址需填入mysql镜像的名称(与docker-compose.yml中的配置相对应),数据库名与 mysql.env 中创建的数据库名一致如果出现以上界面,只需要按照提示在你的typecho软件目录下新建config.inc.php文件,并写入内容即可5.博客迁移迁移时只需要把整个blog目录打包传输至安装有docker和docker-compose的新服务器,然后重新编排即可docker-compose up -d如果是将原有博客结构升级为docker容器的话,只需要将你的typecho文件夹复制到www文件夹中,然后将你的数据库sql文件导入到新建的数据库内即可!PS:记得修改config.inc.php数据库配置和mysql.env对应噢,或者你直接把config.inc.php删掉,重新初始化一下6.参考资料通过docekr搭建typecho详细教程
2021年02月26日
350 阅读
0 评论
0 点赞
2021-02-26
tomcat服务器更改默认项目
Tomcat服务器默认主页是tomcat主页,我们可以将其改为自己的项目,便于访问管理编辑Tomcat安装目录下conf/server.xml文件在<Host></Host>标签之间添加以下代码:<Context path="" docBase="你在webapps下的项目" reloadable="true" />如图:重启tomcat即可
2021年02月26日
116 阅读
0 评论
0 点赞
1
2
...
5