JavaWeb关于高并发,线程,同步化,堆与栈的各类问题
Synchronized同步化的四种方法!
对于同步化,有几点需要声明以便理解:类是被声明的,对象是按照类的具体声明来决定怎样被创建.每个对象都是拥有一把锁的!在多线程对同一对象进行操作的情况下,这把锁的获取权,将会被各个线程锁争夺,唯有获取了该对象的锁,才能对该对象进行操作!
假定现在有一个类:public class Account 代表账户
声明如下:
public class Account {
private int balance; //代表余额
private String name; //用户的姓名
public int deposit (int count){ //入账,存入金额
this.balance += count;
return this.balance;
}
public int chargeoff(int count){ //出账,取出金额
this.balance -= count;
return this.balance;
}
//setter and getter
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
对象(Object)的同步化:
例如:我们在其他的类中填写代码的时候要使用到Account这个类,来操作用户的账户.
假设这个类是public class Operation
public class Operation {
public static void operate(count1, count2){
Account a1 = new Account();
Account a2 = new Account();
synchronized(a1){
a1.deposit(count1);
a1.chargeoff(count2);
}
a2.deposit(count1);
a2.chargeoff(count2);
}
}
如果想要使用Operation这个类,执行对两个账户a1,a2的修改,那么锁住对象变得很关键了.
在实际环境中,若果对a1或者a2两个变量在一小段时间内,同时多线程操作,那么thread-1和thread-2获取的balance很可能是一样的,这样一来,在thread-1还没有操作完毕之后,thread-2所读取的balance是thread-1操作到一半或者是还没有操作的数值.这样的话,会产生”脏数据”.对此,我们给a1这样的对象加上了锁,线程必须取得这样的一把唯一的锁才能对该对象进行操作,让他不像a2那样有着被多条(两条以上)的线程同时操作,避免了脏数据的产生.
值得注意的是:在今天的web项目开发中,用的更多的是分布式开发形式:
用户的数据一般都是被持久化地放在了数据库中(比如MySQL),由于是分布式开发,数据库不止在一台机器上,那么这样的分布式项目所要面临的是数据库之间的共享资源不一致的情况.
因此,为了解决这个问题,我们就必须引入「分布式锁」.这个之后再说.
类中方法(Method)的同步化
例如:
public synchronized int deposit (int count){ //入账,存入金额
this.balance += count;
return this.balance;
}
public synchronized int chargeoff(int count){ //出账,取出金额
this.balance -= count;
return this.balance;
}
这样的方法被同步化了以后,当他的所属类的实例化对象在使用这个同步化方法的时候,该实例化对象的锁将会被线程锁争夺,夺取了锁才能获得方法的执行权.
代码块(Code Block)的同步化:
synchronized{
this.balance += count;
this.balance -= count;
}
一次只有一个线程进入并执行该代码块,锁依旧属于对象,是含有这个代码块的对象.
类(class)的同步化:
涵盖以上三种同步化,是同步化的高达范围.
只要是按照该类创建出来的对象,都会自动地带上锁,以便线程的操作.
Java线程池
Java通过java.util.concurrent.Executors提供四种线程池,分别为:
newCachedThreadPool缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。一般线程的个数是cpu核数的一倍或者二倍.如果i7-8750H是六核的,那么可以设为12或者6.
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
大部分GUI程序都是单线程的,甚至很多大型游戏都是单线程的,这就导致了硬件的一部分浪费,一核工作多核围观的情况!很多都没有多线程优化.
ExecutorServiceexecutorService1
= Executors.newFixedThreadPool(12);//十二个线程:Intel Core i7-8750H cpu核数的二倍
executorService1.submit(newRunnable() { //线程执行器 提交任务@Overridepublic voidrun() {
//要执行的方法}
});
ExecutorServiceexecutorService2 = Executors.newScheduledThreadPool(6);((ScheduledExecutorService) executorService2).schedule(newRunnable() {
@Overridepublic voidrun() {
//要执行的方法}
},100,TimeUnit.SECONDS);//意味着10秒执行一次
ExecutorServiceexecutorService3 = Executors.newCachedThreadPool();executorService3.execute(newRunnable() {
@Overridepublic voidrun() {
//要执行的方法}
});
ExecutorServiceexecutorService4 = Executors.newSingleThreadExecutor();executorService4.submit(newRunnable() {
@Overridepublic voidrun() {
//要执行的方法}
});
该类各部分的关系如下:
public interfaceExecutorServiceextendsExecutor
public interfaceExecutor
public staticExecutorServicenewFixedThreadPool(intnThreads)
public staticExecutorServicenewSingleThreadExecutor()
public staticExecutorServicenewCachedThreadPool()
public staticScheduledExecutorServicenewScheduledThreadPool(intcorePoolSize)
分布式锁的实现方式(部分转载自51CTO)
乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。
如图,假设同一个账户,用户A和用户B都要去进行取款操作,账户的原始余额是2000,用户A要去取1500,用户B要去取1000,如果没有锁机制的话,在并发的情况下,可能会出现余额同时被扣1500和1000,导致最终余额的不正确甚至是负数。但如果这里用到乐观锁机制,当两个用户去数据库中读取余额的时候,除了读取到2000余额以外,还读取了当前的版本号version=1,等用户A或用户B去修改数据库余额的时候,无论谁先操作,都会将版本号加1,即version=2,那么另外一个用户去更新的时候就发现版本号不对,已经变成2了,不是当初读出来时候的1,那么本次更新失败,就得重新去读取最新的数据库余额。
通过上面这个例子可以看出来,使用「乐观锁」机制,必须得满足:
(1)锁服务要有递增的版本号version
(2)每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号
悲观锁也叫作排它锁,在Mysql中是基于 for update 来实现加锁的:
共享锁(lock in share mode)和排他(for update)锁:
u_user:
没什么好讲的,在Navicat中开2个窗口尝试就知道了;
窗口1:
BEGIN;
select * from u_user where id = 1 for UPDATE;
select * from u_user where id = 1 lock in share mode;
COMMIT;
窗口2:
select * from u_user where id = 1 for UPDATE;
select * from u_user where id = 1 lock in share mode;
select * from u_user where id = 1 ;
UPDATE u_user set address="bjpowernode " where id =1 ;
# isnert udpate delete 自动加排他锁 for update
UPDATE u_user set address="bjpowernode " where id =1 for update;
UPDATE u_user set address="bjpowernode " where id =1 lock in share mode;
小小的总结:共享锁(lock in share mode)在事务开启的情况下,其他窗口的加了共享锁的语句会遭遇堵塞.一定要等之前的线程执行完毕之后才能轮到下一次查询.每一个窗口相当于一个线程,这是数据库自己的线程.
Update , insert , delete 三种语句不可以再后面加 for update或者lock in share mode,否则客户端会提示语法错误.三者在sql内,是默认加锁的.
事务没有完结,等待的线程就会进入堵塞状态.
Commit之后,update的执行就开始了.
基于ZooKeeper实现
其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。
原理就是:当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。
当释放锁的时候,只需将这个临时节点删除即可.
JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
栈区:
每个线程包含一个栈区,栈中只保存方法中(不包括对象的成员变量)的基础数据类型和自定义对象的引用(不是对象),对象都存放在堆区中
每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
存储的全部是对象实例,每个对象都包含一个与之对应的class的信息(class信息存放在方法区)。
jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身,几乎所有的对象实例和数组都在堆中分配。
又叫静态区,跟堆一样,被所有的线程共享。它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆区:
方法区: