Java程序的进程里有几个线程:主线程,垃圾回收线程(后台线程)等
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
Java支持多线程,当Java程序执行main方法的时候,就是在执行一个名字叫做main的线程,可以在main方法执行时,开启多个线程A,B,C,多个线程 main,A,B,C同时执行,相互抢夺CPU,Thread类是java.lang包下的一个常用类,每一个Thread类的对象,就代表一个处于某种状态的线程
多个线程或进程”同时”运行只是感官上的一种表现。事实上进程和线程是并发运行的,操作系统的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行。微观上所有进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发。
并行是指在多CPU或者多核心CPU的场景下,线程不共享CPU,每个线程一个CPU,同时执行多个任务的场景。
状态:
Java中的六种线程状态
状态名称 | 说明 |
NEW | 初始状态,线程刚被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java系统系统中将操作系统中的就绪和运行两种状态笼统地称为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITTING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程做出一些特定动作(通知或者中断) |
TIME_WAITTING | 超时等待状态,该状态不同于等待状态,它可以在指定的时间后自行返回 |
TERMINATED | 中止状态,表示当前线程已经执行完毕 |
public class TestThread {
static String content;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run() {
content="helloWrold";//正常进入run方法后就代表进入了运行状态,但是如果是直接调用t.run()就不是多线程了,只是一个普通方法
};//run方法执行结束,线程处于死亡状态
};
t.start();//就绪状态,等待CPU分配时间片即可进入下一个运行状态
//Thread.sleep(1000);//主线程阻塞
t.join();//让t线程执行结束,主线程阻塞
while (content==null){
Thread.yield();//让当前线程放弃CPU,处于就绪的状态
}
System.out.println(content.toUpperCase());
}
}
多个线程并发执行时,能否保证数据的正确性
如何保证线程安全,通过保证操作的原子性
class counter01{
private volatile int count;
public synchronized void count(){//synchronized在jdk1.6之前性能差,所以才有Lock
count++;
}//排它锁,独占锁(只有一个线程能进来,其他线程拿不了),非公平锁(谁的线程优先级高,谁抢到就谁执行,有些线程会因为优先级低而一直无法执行)
}
class counter02{
private volatile int count;
private Lock lock=new ReentrantLock(true);//公平锁(拿不到锁的线程有机会拿到锁),公平性会导致性能差一些
public void count(){
lock.lock();//加锁
try {
count++;
}finally {
lock.unlock();//解锁
}
}
}
a)内存地址
b)期望数据值
c)需要更新的值
CAS算法支持无锁状态下的并发更新,但可能回出现ABA问题,长时间自旋问题
class counter03{
//底层使用CAS算法实现,Atomic开头的类
private AtomicInteger at=new AtomicInteger();
public void count(){
at.getAndIncrement();
}
}
Connection数据库连接对象
SimpleDateFormat日期格式化对象
SqlSession对象
都不允许共享,每个线程一个
class DateUtils{
private static ThreadLocal<SimpleDateFormat> td=new ThreadLocal<>();
public static String format(Date date){
SimpleDateFormat sdf = td.get();
if(null!=sdf){
return sdf.format(date);
}else {
sdf=new SimpleDateFormat("yyyy/MM/dd");
td.set(sdf);
return sdf.format(date);
}
}
}
说明:Java中的线程安全问题的主要关注点有3个:可见性,有序性,原子性;
Java内存模型JMM解决了可见性和有序性问题,而锁解决了原子性问题。
Synchronized是排它锁的一种实现,支持可重入性
基于这种机制可以实现多线程在共享数据集上的同步(互斥与协作)
互斥:多线程在共享数据集上排队执行
协作:多线程在共享数据集上进行协作执行(通讯)
说明:
排他性:如果线程T1已经持有锁L,则不允许除T1外的任何线程持有该所L
重入性:如果线程T1已经持有锁L,则允许线程T1多次获取锁L,更确切的说,获取一次后,可多次进入锁
java中所有对象都继承自Object,所有对象都有Monitor
为了减少获得锁和释放锁带来的性能消耗,JDK1.6以后的锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁装填、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级。
锁可以升级但不能降级,这种策略的目的是为了提高获得锁和释放锁的效率。
一般用于修饰属性变量,volatile的底层是通过内存屏障来禁止指令重排序
重排序有以下2个条件:
读写锁策略(一个写,并发读,类似读写锁)
class counter01{
private volatile int count;
public int getCount(){//read
return count;
}
public synchronized void doCount(){//write
count++;
}
}
在JMM中,如果一个操作的执行结果要对另一个操作可见,那么这两个操作之间必须存在happens-before关系,它是判断数据是否存在竞争,线程是否安全的主要依据
JAVA中为了保证多线程并发访问的安全性,提供了基于锁的应用,可以分为2类:悲观锁和乐观锁
悲观锁:假定会发生并发冲突,所以会屏蔽一切可违反数据完整性的操作,同一时刻只能有一个写操作。
例如synchronized,Lock,ReadWriteLock,数据库悲观锁等实现
数据库悲观锁:
通过数据库锁机制实现,即对查询语句添加for update关键字。
如下sql语句 select * from table where id = 1 for update 当一个请求A开启事务并执行此sql同时未提交事务时,另一个线程B发起请求,此时B将阻塞在加了锁的查询语句上,直到A请求的事务提交或者回滚,B才会继续执行,保证了访问的隔离性。
乐观锁:假定不会发生并发冲突,只在提交操作时检查是否违反数据完整性,多个线程可以并发执行写操作但是只有一个成功。
基于CAS算法实现的Atomic包下的类,还有数据库乐观锁等
数据库乐观锁:
2.1 借助数据库表增加一个版本号的字段version,每次更新一行记录,都使得该行版本号加一,开始更新之前先获取version的值,更新提交的时候带上之前获取的version值与当前version值作比较,如果不相等则说明version值发生了变化则检测到了并发冲突,本次操作执行失败,如果相等则操作执行成功。
例如:update table set columnA = 1,version=version+1 where id=#{id} and version = #{oldVersion}
2.2 借助行更新时间时间戳,检测方法则与方式1相似,即更新操作执行前先获取记录当前的更新时间,在提交更新时,检测当前更新时间是否与更新开始时获取的更新时间时间戳相等
悲观锁适合写操作比较多的场景,加锁可以保证写操作时数据正确
乐观锁适合读操作比较多的场景,不加锁能使其读操作的性能大幅提升
一个线程得到CPU执行时间是有限的,当此线程用完了为其分配的CPU时间以后,CPU会切换到下一个线程执行。
在线程切换之前,线程需要将当前的状态进行保存,以便下次再次获得CPU时间片之后可以加载对应的状态以继续执行剩下的任务。而这个切换过程是需要韩飞时间的,会影响多线程程序的执行效率,所以在使用多线程时要减少线程的频繁切换。
多个线程互相等待被对方线程正在占用的锁,导致陷入彼此等待对方是否锁的状态,这个过程称之为死锁。