线程安全
1.如果多个线程在同时运行,而这些线程可能会同时运行这段代码,程序每次运行结果和单线程运行的结果是一样的,而且其他变量的值也和预期的是一样的,这就是线程安全的。
2.产生线程安全问题的原因:多个线程同时对成员变量进行写的操作的时候,没有进行数据同步或者其他处理,多个线程就会产生线程安全问题。
线程同步
Java中提供了同步机制(synchronized)来解决,解决的原理就是,当某个线程进入代码执行的时候,其他线程只能在外面等着,当这个线程操作结束,其他线程才有机会进入这个代码块去执行。
同步的三种方式
1.同步代码块
将需要同步操作的代码块用synchronized关键字包裹起来,就实现了同步。
synchronized(同步锁){
需要同步操作的代码
}
同步锁:同步锁只是一个概念,多个线程来争抢这把锁,谁拿到了,谁就可以进入执行这个代码块,其他线程只能在外面等待,等到线程执行完毕会把锁归还给cpu,这时线程会重新争抢锁,谁抢到了谁就执行,如此循环。
同步锁的特点:①锁对象可以是任意类型,只要是Object的子类;②多个线程对象,要使用同一把锁。注意:在任何时候,多个线程看到的必须是同一把锁,最多只允许一个线程拥有同步锁才能实现线程互斥,谁拿到锁谁就进入代码块,其他线程只能在外面等着。
public class Tickets implements Runnable {
public static int p=50;
@Override
public void run() {
while (true){
synchronized ("str"){
if(p>0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
p--;
System.out.println(Thread.currentThread().getName()+"正在售票,剩余票数---"+p);
}
}
}
}
}
public class Ticket_test {
public static void main(String[] args) {
Tickets t1 = new Tickets();
//四个线程共享同一个资源,一个进程获取到锁后,其他线程就要等待
Thread td1=new Thread(t1);
Thread td2=new Thread(t1);
Thread td3=new Thread(t1);
Thread td4=new Thread(t1);
td1.start();
td2.start();
td3.start();
td4.start();
}
}
2.同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着
public synchronized void method(){
可能会产生线程安全问题的代码
}
例如:
public class Tickets implements Runnable {
private int p=50;
@Override
public void run() {
while (true) {
test();
}
}
public synchronized void test(){
if (p > 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
p--;
System.out.println(Thread.currentThread().getName() + "正在售票,剩余票数---" + p);
}
}
3.Lock锁
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象
1.Lock锁也称同步锁,加锁与释放锁方法化了
①public void lock() :加同步锁
②public void unlock():释放同步锁
public class Tickets implements Runnable {
private int p=50;
//创建锁对象
ReentrantLock ren =new ReentrantLock();
@Override
public void run() {
while (true) {
//加锁
ren.lock();
if (p > 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
p--;
System.out.println(Thread.currentThread().getName() + "正在售票,剩余票数---" + p);
}
//释放锁
ren.unlock();
}
}
}
死锁
互相等待对方资源,称之为死锁。
比如:线程1持有ObjectA上的锁,并等待ObjectB上的锁,线程2持有ObjectB上的锁,并等待ObjectA上的锁,每个线程都不放弃获得的锁,并一直等待第二个锁,因此会永远等待下去。这样称之为死锁
线程协作
线程协作是通过线程之间的握手来实现的,这种握手可以通过Object的wait()和notify()来安全的实现。
1.wait():让线程处于等待状态,以链表的形式挂在该锁下,并且释放所持有的锁。
2.notify():唤醒处于等待状态的线程,如果有多个线程等待,则唤醒该锁下处于等待状态的第一个线程,并且释放所持有的锁。
3.notifyAll():唤醒该锁下所有等待的线程。并且也释放所持有的锁。
生产者和消费者的问题
有1个消费者来吃,1个生产者来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则生产者停止做汉堡,如果消费者发现容器内的汉堡吃完了,就提醒生产者再做几个汉堡出来。此时服务员过来安抚消费者,让他等待。而一旦生产者的汉堡做出来,就会让服务员通知消费者,汉堡做好了,让消费者继续过来取汉堡。
public class Producer implements Runnable{
//生产者
public static ArrayList<Integer> arr=new ArrayList<>();
@Override
public void run() {
int count=0;
while (true){
if(arr.size()<10){
//做汉堡
count++;
arr.add(count);
System.out.println("当前制作了汉堡:"+count);
//通知客户来吃
synchronized (arr){
arr.notify();//唤醒等待的线程,并释放持有的锁
}
}else{
synchronized (arr){
try {
arr.wait();//线程处于等待状态,但是会把持有的锁释放
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Consumer implements Runnable {
//消费者
@Override
public void run() {
while (true){
if(Producer.arr.size()>0){
//吃汉堡
Integer r = Producer.arr.remove(0);
System.out.println("当前正在吃汉堡"+r);
//通知厨师来做
synchronized (Producer.arr){
Producer.arr.notify();//唤醒等待的线程,并释放持有的锁
}
}else{
synchronized (Producer.arr){
try {
Producer.arr.wait();//让线程处于等待状态,并且释放持有的锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class test {
public static void main(String[] args) {
Producer p = new Producer();
Consumer c = new Consumer();
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
sleep与wait的区别:
sleep:让线程睡眠指定的时间,不释放锁。
wait:让线程处于等待状态,释放锁;并且可以通过notify唤醒该锁下处于wait状态的线程,但是notify不能唤醒sleep的线程。
线程池
没有线程池的状态:
我们要使用一条线程的时候,先将线程对象创建出来,启动线程,在运行过程中,可能能完成任务,也可能会在中途被任务内容中断掉,但是任务还没有完成;即使能够正常完成,线程对象就结束了,就变成了垃圾对象,需要被垃圾回收器回收;如果系统中,大量的任务都是小任务,任务消耗的时间较短,线程对象的创建和消亡耗费的时间比较多,这样就会导致大部分的时间都浪费在了线程对象的创建和死亡上;如果任务本身破坏力较大,可能会把线程对象结束掉,就无法继续完成任务
有线程池的时候:
在没有任务的时候,先把线程对象准备好,存储到一个容器中,一旦有任务来的时候,就不需要创建对象,而是直接将对象获取出来执行任务;如果任务的破坏力较小,任务可以直接完成,这个线程对象不会进入死亡状态,而是被容器回收,继续活跃;如果任务的破坏力较大,任务会把线程搞死,线程池会继续提供下一个线程,继续完成这个任务;除此之外,使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数可以控制系统中并发线程数目不超过最大线程数。
线程池的好处:
1.降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可以执行多个任务。
2.提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
3.提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而让服务器崩溃。(每个线程大约需要1MB内存,线程开的越多,消耗的内存也就越大)
线程池创建方式
Java线程池中的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池原理不是很清楚的情况下,很可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池,建议使用Executors工程类来创建线程池中的对象。
对线程池进行配置:
1.配置线程池最大数量:最多可以并发执行的最大的线程数。
2.配置最长等待时间:如果任务数大于此线程池最大的线程数,那么底层会配置一个最大等待时长,如果线程池中没有空闲的线程,那么线程会等待一个最大等待时长,在这期间,如果线程池有空闲的线程,就执行正在等待的任务;如果没有空闲的线程,线程池会再创建新的线程去执行任务。
Executors创建线程池对象的方法:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
//创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(100);
使用线程池对象的方法:
1.public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
public class Exe implements Runnable {
@Override
public void run() {
for (int i=0;i<=50;i++){
System.out.println("线程池线程正在执行==="+i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Executor_test {
public static void main(String[] args) {
//创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(100);
//执行任务
Exe e1 = new Exe();
es.submit(e1);
es.submit(e1);//同一个线程体可以有多个线程执行
}
}
2.public Future submit(Callable task):将Callable对象传给submit,Future是 call()方法调用后的返回值
注:Callable接口是对Runnable接口的增强,提供了一个call()方法,作为线程的执行体,但call()方法比run()方法功能更加强大。call()方法有返回值,可以抛出异常;而run方法不能抛出异常,并且没有返回值
public class Callable_test implements Callable<Integer> {//当前泛型的是规定返回值的类型
@Override
public Integer call() throws Exception {
int i=1;
for(;i<=30;i++){
System.out.println(i);
Thread.sleep(200);
}
return i;
}
}
public class Cal {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(20);
Future<Integer> a = es.submit(new Callable_test());
//i是线程运行完毕之后返回的值
Integer i = a.get();
System.out.println(i);//31
}
}
关闭线程池的方法(一般不关闭)
1.shutdown():调用这个方法后,线程池将不会再接受新任务,但原有的任务会继续执行,直到执行完成
2.shutdownNow():调用这个方法关闭线程池,没有执行完成的任务会暂停