一些JavaSE学习过程中的思路整理(三)(主观性强,持续更新中...)

未经作者允许,不可转载,如有错误,欢迎指正o( ̄▽ ̄)o,安利一位b站的up:楠哥教你学Java,干货比较多

Java线程同步的几种常见情况分析

同步问题针对的是多个线程所共享的资源,下面分几步假设了一个思考的过程:

  • 1.多个线程共享一个静态成员变量,不进行线程同步处理,直接打印这个静态变量,果然出现问题
public class Test1 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Account account = new Account();
Thread thread = new Thread(account);
thread.start();
}
}
} class Account implements Runnable {
private static int num = 0; @Override
public void run() {
num++;
//此时run没有上锁且如果没有睡眠则可能产生错误,但不一定会有错误
//但是如果加入睡眠则会出现问题,因为num是共享的,某个
//线程执行完num++后可能被挂起,其他线程再次执行num++以此类推
//直到最早从挂起恢复到就绪态的那个线程去执行打印操作时
//num已经不是预期中的数字
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + num + "次访问");
}
}
  • 2.在1的基础上,对run方法用synchronized关键字修饰,试图上锁实现线程的同步,但是发现打印结果依旧没有变化,五次打印结果依旧出现重复
public class Test1 {
public static void main(String[] args) { for (int i = 0; i < 5; i++) {
Account account = new Account();
Thread thread = new Thread(account);
thread.start();
}
}
} class Account implements Runnable {
private static int num = 0; //单单是用synchronized修饰后虽然该run方法上锁,但是回到主函数中观察
//我们发现5次循环,每次都new了一个account对象,用新的account去创建
//Thread类对象,所以每次线程开启后所上锁的是5个独立的run方法,依旧
//无法做到某个线程在调用run方法时能独占CPU资源的意图(各自锁各自的run方法)
@Override
public synchronized void run() {
num++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + num + "次访问");
}
}
  • 3.为了解决2的问题,将 Account account = new Account(); 向上挪动以下即可
public class Test1 {
public static void main(String[] args) {
//只需要将account对象作为公共的对象引用,则它的run方法在被某个
//线程占用的时候就能实现该线程独占CPU资源的操作,从而确保线程同步
Account account = new Account();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(account);
thread.start();
}
}
} class Account implements Runnable {
private static int num = 0; @Override
public synchronized void run() {
num++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + num + "次访问");
}
}
  • 4.对于静态方法,即使每次都实例化一个类的对象,依旧拥有线程同步的功能,因为静态方法是属于类的,而不是属于类的实例的,只要该方法上锁,那个调用这个方法的线程就可以在方法调用结束之前独占CPU的资源,而其他想访问这个方法的线程将被阻塞
public class Test1 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
//这里用了匿名内部类的方式,在run中调用Test1类的静态方法
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//虽然这里每次都新建了一个类,但是由于test方法是静态方法,是属于类的共享资源
//所以每个线程调用上锁的test方法后,可以由该线程独占CPU资源直到方法结束
Test1 test1 = new Test1();
test1.test();
}
});
thread.start();
}
} public static synchronized void test() {
System.out.println(".....start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(".......end");
}
}

由简单到复杂的几种单例模式写法

  • 单线程模式下的单例模式,但是这种在多线程下无法实现线程安全(线程同步)
public class Test1 {
public static void main(String[] args) {
//对main来说是调用了SingletonDemo类的静态方法
SingletonDemo singletonDemo = SingletonDemo.getInstance();
SingletonDemo singletonDemo1 = SingletonDemo.getInstance();
}
} class SingletonDemo {
//静态成员变量会默认初始化为(0,false,null)
private static SingletonDemo singletonDemo; //构造方法是属于实例的,所以不能是静态的
private SingletonDemo() {
System.out.println("创建了SingletonDemo对象实例");
} //静态方法只能调用静态变量与方法
//但是静态方法能够调用构造方法(一定是非静态的),因为静态方法不需要对类进行实例化就能调用
//而静态方法之所以无法调用非静态方法是因为非静态方法需要实例化后才能调用,而构造方法很特殊,它
//用于创建一个新的对象引用后返回该引用,即使在静态方法中调用,也不会出现未实例化而无法调用的情况
public static SingletonDemo getInstance() {
if (singletonDemo == null)
singletonDemo = new SingletonDemo();
return singletonDemo;
}
}
  • 多线程模式下的单例模式
public class Test1 {
public static void main(String[] args) {
//为了创建例子方便,只在堆内存中进行创建对象引用实例并直接开启线程
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo.getInstance();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo.getInstance();
}
}).start();
}
} class SingletonDemo {
//静态成员变量会默认初始化为(0,false,null)
private static SingletonDemo singletonDemo; //构造方法是属于实例的,所以不能是静态的
private SingletonDemo() {
System.out.println("创建了SingletonDemo对象实例");
} //静态方法只能调用静态变量与方法
//但是静态方法能够调用构造方法(一定是非静态的),因为静态方法不需要对类进行实例化就能调用
//而静态方法之所以无法调用非静态方法是因为非静态方法需要实例化后才能调用,而构造方法很特殊,它
//用于创建一个新的对象引用后返回该引用,即使在静态方法中调用,也不会出现未实例化而无法调用的情况
public synchronized static SingletonDemo getInstance() {
if (singletonDemo == null)
singletonDemo = new SingletonDemo();
return singletonDemo;
}
}
  • 双重监测,synchronized 关键字修饰代码块
  1. 线程同步是为了实现线程安全,如果只创建一个对象,那么线程就是安全的
  2. 如果 synchronized 锁定的是多个线程共享的数据(同一个对象),那么线程就是安全的
  3. volatile 的作用使得主内存中的数据对象对线程直接可见,而不用通过工作内存,一般情况下是不可见的,需要通过工作内存对主内存中需要操作的对象进行拷贝
public class Test1 {
public static void main(String[] args) {
//为了创建例子方便,只在堆内存中进行创建对象引用实例并直接开启线程
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo.getInstance();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo.getInstance();
}
}).start();
}
} class SingletonDemo {
//静态成员变量会默认初始化为(0,false,null)
private static volatile SingletonDemo singletonDemo; //构造方法是属于实例的,所以不能是静态的
private SingletonDemo() {
System.out.println("创建了SingletonDemo对象实例");
} //静态方法只能调用静态变量与方法
//但是静态方法能够调用构造方法(一定是非静态的),因为静态方法不需要对类进行实例化就能调用
//而静态方法之所以无法调用非静态方法是因为非静态方法需要实例化后才能调用,而构造方法很特殊,它
//用于创建一个新的对象引用后返回该引用,即使在静态方法中调用,也不会出现未实例化而无法调用的情况
public static SingletonDemo getInstance() {
if (singletonDemo == null) {
//因为这个类一定是唯一的,所以可以通过锁类达到锁定部分代码块的目的
//但是需要注意,锁定某个类与该类中的具体逻辑无关?
synchronized (SingletonDemo.class) {
if (singletonDemo == null)
singletonDemo = new SingletonDemo();
}
}
return singletonDemo;
}
}

死锁的实现与破解

  • 死锁的产生:由于多个线程同时对多个共享的资源进行抢占,导致某一时刻线程运行所需的资源分布在不同的线程中,而对应的多个线程都无法继续执行下去(如完成两个线程都需要对应的两个资源,但是某一时刻甲占了一个,乙占了另一个,都无法继续执行则两个线程都停在原地等待资源,虽然可能永远也等不到...)
public class DeadLockTest {
public static void main(String[] args) {
//因为是继承了Runnable接口的实现类,所以不能用匿名内部类的方式调用
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1, "张三").start();
new Thread(deadLockRunnable2, "李四").start();
}
} class DeadLockRunnable implements Runnable {
public int num;
//对于静态的类变量采用声明时直接调用构造函数初始化的操作
private static Chopsticks chopsticks1 = new Chopsticks();
private static Chopsticks chopsticks2 = new Chopsticks(); @Override
public void run() {
if (num == 1) {
synchronized (chopsticks1) {
System.out.println(Thread.currentThread().getName() + "拿到筷子一,等待获取筷子二");
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chopsticks2) {
System.out.println(Thread.currentThread().getName() + "获取筷子二,完成就餐");
}
}
} else {
synchronized (chopsticks2) {
System.out.println(Thread.currentThread().getName() + "拿到了筷子二,等待获取筷子一");
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chopsticks1) {
System.out.println(Thread.currentThread().getName() + "拿到了筷子一,完成就餐");
}
}
}
}
} class Chopsticks {}
  • 破解死锁:依旧以上一个两支筷子的代码为例,在第二个线程开启前,使主线程休眠一段时间,让第一个线程充分使用资源并完成运行,这样第二个线程开启后将获得全部的资源
new Thread(deadLockRunnable1, "张三").start();
//小技巧,shift + tab,使得选中部分向左侧移动一个tab
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(deadLockRunnable2, "李四").start();

使用lambda表达式化简代码

以函数式接口为参数的方法,可以通过写入lambda表达式达到相同的效果,其中lambda表达式结构为()->{},其中()位置为原本函数式接口需要实现的方法的形式参数,{}中为该函数式接口需要实现的方法的方法体,可以使用()中的参数

import java.util.concurrent.TimeUnit;

public class Test1 {
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "----------start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-----------end");
}, "张三").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "----------start");
try {
//这个方法进一步封装了休眠的方法,使用更加灵活
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-----------end");
}, "李四").start();
}
}

JUC包的Lock接口与ReentrantLock(重入锁)

多线程的实现有两种方式,一种是继承Thread类重写run方法的方式,另一种是实现Runnable接口的方式,普遍认为后一种方式更好,因为一定程度上进行了解耦合,将任务的实现与线程分离

示例代码通过实现Runnable接口的方式展示了重入锁的使用(ReentrantLock就像是手动挡的汽车,相比于synchronized使用更为灵活):

public class Test1 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable, "张三").start();
new Thread(myRunnable, "李四").start();
}
} class MyRunnable implements Runnable {
private static int num;
private Lock lock = new ReentrantLock(); @Override
public void run() {
lock.lock();
//可以重复上锁
lock.lock();
num++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "是当前第" + num + "位访客");
lock.unlock();
//
lock.unlock();
}
}

在上述代码的基础上可以继续实现资源(实现类)和Runnable接口的解耦合(对此我有点不太理解,是因为类没有继承Runnable接口就是解耦合吗?)

public class Test1 {
public static void main(String[] args) {
new Thread(()->{
MyRunnable.count();
}, "张三").start();
new Thread(()->{
MyRunnable.count();
}, "李四").start();
}
} class MyRunnable {
//设置线程可见可以进一步提高准确度
private volatile static int num;
private static Lock lock = new ReentrantLock(); public static void count() {
//关于上锁的内容,一定要明确是多个线程共享的资源,否则无效
lock.lock();
num++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"是第" + num + "位访客");
lock.unlock();
}
}

ReentrantLock 的时限性:设定一个时间供某个线程获得锁,并返回一个boolean值,通过ReentrantLock对象的tryLock方法,tryLock(long time, TimeUnit unit),time指时间数值,unit指时间单位

public class Test1 {
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
/*
张三尝试拿锁成功,拿到锁,执行业务代码,休眠5秒钟
李四尝试6秒内拿锁失败
*/
new Thread(()->{
timeLock.lock();
}, "张三").start();
new Thread(()->{
timeLock.lock();
}, "李四").start();
}
} class TimeLock {
private ReentrantLock reentrantLock = new ReentrantLock(); public void lock() {
//尝试在三秒内获得锁
try {
//尝试在指定时间内拿锁,拿到就上锁
if(reentrantLock.tryLock(6, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "get lock");
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println(Thread.currentThread().getName() + "not lock");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//如果拿到锁了这里的boolean值就是true,需要解锁
if (reentrantLock.isHeldByCurrentThread()) {
reentrantLock.unlock();
}
}
}
}

生产者消费者模式

容器类

public class Container {
public Hamburger[] array = new Hamburger[6];
public int index = 0; //往柜台放入汉堡需要上锁
public synchronized void push(Hamburger hamburger) {
//如果没有空余位置可以放汉堡则每当生产者线程执行到这里就会
//被暂停(阻塞),由运行态变为就绪态,下一次被分配到CPU时
//从上次执行的位置开始继续执行,所以这里要用while而不是if
//因为下一次可能容器依旧没有空闲的位置,每次都需要进行index的判断
while (index == array.length) {
try {
//暂停访问当前资源的线程
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//重启被暂停(阻塞)的线程
this.notify();
array[index++] = hamburger;
System.out.println("生产了一个汉堡" + hamburger);
} //从柜台取出汉堡也需要上锁
public synchronized Hamburger pop() {
//每次被阻塞,变为就绪态,再从就绪态变为运行态后都是从上次
//执行的位置继续执行,所以要while循环每次都要在取出汉堡前检测index
while (index == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
System.out.println("消费了一个汉堡" + array[--index]);
return array[index];
}
}

汉堡类

public class Hamburger {
private int id; public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} public Hamburger(int id) {
this.id = id;
} @Override
public String toString() {
return "Hamburger{" +
"id=" + id +
'}';
}
}

生产者类

public class Producer {
private Container container; public Producer(Container container) {
this.container = container;
} public void produce() {
for (int i = 0; i < 15; i++) {
Hamburger hamburger = new Hamburger(i);
//核心在于调用容器类的放入汉堡方法
this.container.push(hamburger);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

消费者类

public class Consumer {
private Container container; public Consumer(Container container) {
this.container = container;
} public void consume() {
for (int i = 0; i < 15; i++) {
//核心在于调用容器的取出汉堡的方法
this.container.pop();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

测试类

public class Test1 {
public static void main(String[] args) {
Container container = new Container();
//这里生产者和消费者贡献容器类的实例
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
//无论开启多少个生产者或者消费者的线程,它们都是共享容器资源的
//所以对容器的实例方法上锁就能达到线程同步(线程安全)的需求
new Thread(()->{
producer.produce();
}).start();
new Thread(()->{
producer.produce();
}).start();
new Thread(()->{
consumer.consume();
}).start();
new Thread(()->{
consumer.consume();
}).start();
new Thread(()->{
consumer.consume();
}).start();
}
}

多线程并发卖票

三个窗口并发出售15张票的例子

资源类(Ticket)

public class Ticket {
private int surpluConut = 15;
private int outCount = 0;
private ReentrantLock reentrantLock = new ReentrantLock(); public int getSurpluConut() {
return surpluConut;
} public void sale() {
reentrantLock.lock();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//先检查剩余票数
if(surpluConut == 0)
System.out.println(Thread.currentThread().getName() + "已售罄");
else {
surpluConut--;
outCount++;
System.out.println(Thread.currentThread().getName() + "售出第" + outCount + "张票");
if (surpluConut == 0) System.out.println(Thread.currentThread().getName() + "已售罄");
}
reentrantLock.unlock();
}
}

测试类

public class Test1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
while (ticket.getSurpluConut() > 0) {
ticket.sale();
}
}, "A").start();
new Thread(()->{
while (ticket.getSurpluConut() > 0) {
ticket.sale();
}
}, "B").start();
new Thread(()->{
while (ticket.getSurpluConut() > 0) {
ticket.sale();
}
}, "C").start();
}
}

多线程并发卖票的另一种写法,都看明白了你就懂了

资源类

public class Ticket {
private int surpluConut = 15;
private int outCount = 0;
private ReentrantLock reentrantLock = new ReentrantLock(); public int getSurpluConut() {
return surpluConut;
} public void sale() {
while (surpluConut > 0) {
//这个重入锁要锁在while循环内部,否则其余线程分配到资源后遇到lock被阻塞,将任由第一个获得锁的线程完成所有票的出售
//而lock放在while内部会使得每次占用锁的线程只会独占while的一轮循环,一轮循环结束后释放锁,其余线程也有机会获得锁
reentrantLock.lock();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//先检查剩余票数
if(surpluConut == 0)
System.out.println(Thread.currentThread().getName() + "已售罄");
else {
surpluConut--;
outCount++;
System.out.println(Thread.currentThread().getName() + "售出第" + outCount + "张票");
if (surpluConut == 0) System.out.println(Thread.currentThread().getName() + "已售罄");
}
reentrantLock.unlock();
}
}
}

测试类

public class Test1 {
public static void main(String[] args) {
var ticket = new Ticket();
//任务
Runnable r = () -> {
ticket.sale();
};
//运行机制解耦合
new Thread(r, "A").start();
new Thread(r, "B").start();
new Thread(r, "C").start();
}
}

一些JavaSE学习过程中的思路整理(三)(主观性强,持续更新中...)的更多相关文章

  1. 一些JavaSE学习过程中的思路整理(主观性强,持续更新中...)

    目录 一些JavaSE学习过程中的思路整理(主观性强,持续更新中...) Java书写规范 IDEA的一些常用快捷键 Java类中作为成员变量的类 Java源文件中只能有一个public类 Java中 ...

  2. iOS --- 总结Objective-C中经常使用的宏定义(持续更新中)

    将iOS开发中经常使用的宏定义整理例如以下,仅包括Objective-C. 而对于Swift,不能使用宏,则能够定义全局函数或者extension.请參考博客iOS - 总结Swift中经常使用的全局 ...

  3. 最简单的 IntelliJ IDEA 中使用 GitHub 进行版本控制教程(持续更新中)

    一.在 IntelliJ IDEA 中新建一个项目并提交到 GitHub 1. 运行 IDEA,点击[Create New Project],在 IDEA 中新建一个项目. 2. 在选择项目类型对话框 ...

  4. java视频教程 Java自学视频整理(持续更新中...)

    视频教程,马士兵java视频教程,java视频 1.Java基础视频 <张孝祥JAVA视频教程>完整版[RMVB](东西网) 历经5年锤炼(史上最适合初学者入门的Java基础视频)(传智播 ...

  5. 2020年腾讯实习生C++面试题&持续更新中(3)

    2020年腾讯实习生C++面试题&持续更新中(3) hello,大家好,我是好好学习,天天编程的天天. 来给大家大家分享腾讯实习生面经了. 天天希望大家看到面经后一定要做充分的准备,结合自己掌 ...

  6. fastadmin 后台管理框架使用技巧(持续更新中)

    fastadmin 后台管理框架使用技巧(持续更新中) FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架,具体介绍,请查看文档,文档地址为:https://doc. ...

  7. 2020年腾讯实习生C++面试题&持续更新中(1)

    2020年腾讯实习生C++面试题&持续更新中(1) 腾讯面试整理(1) 最近大三的学生找实习生的同学非常多,给大家分享一篇腾讯实习生的面试题,关于面试题,会持续更新~~~ 也算是今天开通博客的 ...

  8. 《WCF技术剖析》博文系列汇总[持续更新中]

    原文:<WCF技术剖析>博文系列汇总[持续更新中] 近半年以来,一直忙于我的第一本WCF专著<WCF技术剖析(卷1)>的写作,一直无暇管理自己的Blog.在<WCF技术剖 ...

  9. 【前端】Util.js-ES6实现的常用100多个javaScript简短函数封装合集(持续更新中)

    Util.js (持续更新中...) 项目地址: https://github.com/dragonir/Util.js 项目描述 Util.js 是对常用函数的封装,方便在实际项目中使用,主要内容包 ...

  10. 中国.NET:各地微软技术俱乐部汇总(持续更新中...)

    中国.NET:各地微软技术俱乐部汇总(持续更新中...)   本文是转载文,源地址: https://www.cnblogs.com/panchun/p/JLBList.html by ​史记微软. ...

随机推荐

  1. Django框架项目——redis操作、Celery

    1-redis操作 redis介绍 redis安装 """ 1.官网下载:安装包或是绿色面安装 2.安装并配置环境变量 """ redis ...

  2. Xshell链接不上解决问题

    #5.远程连接工具排错? #一.测试网络是否通畅 1.测试网络连通性:ping 服务端ip地址 2.关闭防火墙 systemctl stop firewalld #关闭防火墙 systemctl di ...

  3. [最佳实践]配置sshd只允许sftp登录

    sftp 是 Secure File Transfer Protocol 的缩写,即安全文件传送协议,可为传输文件提供一种安全的加密方法. sftp 为 SSH 的一部分,由于这种传输方式使用了加密/ ...

  4. Python - 打断点以及如何查看

    1.鼠标左键单击代码跟行号中间的地方会出现一个红点,这个就是断点. 2.点击Debug按钮,进入调试模式. 3.当代码运行到断点之前,所有关于变量的代码,都会出现运行的结果. 4.点击Step Int ...

  5. centos7 oracle11gR2安装

    CentOS7安装Oracle 11gR2 图文详解 摘自: http://www.linuxidc.com/Linux/2016-04/130559.htm 最近要运维一个项目,准备在家办公,公司无 ...

  6. k 分算法是 k 越大越好吗?

    引入 我们有二分算法,就是: 定义 二分查找(英语:binary search),也称折半搜索(英语:half-interval search).对数搜索(英语:logarithmic search) ...

  7. (Good topic)哈希表:最长回文串(3.19 leetcode每日打卡)

    给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串. 在构造过程中,请注意区分大小写.比如 "Aa" 不能当做一个回文字符串. 注意: 假设字符串的长度不 ...

  8. Miniconda安装及搭建

    Miniconda安装配置 下载Miniconda Miniconda下载地址 最新版 Miniconda For Windows 下载链接 Windows 安装配置 修改Powershell执行策略 ...

  9. 总结--flask部分

    Flask框架的诞生: Flask诞生于2010年,是Armin ronacher(人名)用Python语言基于Werkzeug工具箱编写的轻量级Web开发框架. Flask本身相当于一个内核,其他几 ...

  10. MySQL-utf8 和 utf8mb4 区别?

    版权声明:原创作品,谢绝转载!否则将追究法律责任. ----- 作者:kirin 1.首先说明一下,版本问题.MySQL8.0之后默认:utf8mb4,而8.0之前默认:latin 2.utf8 和 ...