一(多线程同步)
多线程的并发执行可以提高程序的效率,但是当多个线程去访问同一个资源时,有时也会引发一些安全性问题。例如:统计一个班上的学生人数时,学生有进有出会影响最终学生人数。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
package 多线程;
class TicketThread implements Runnable{
int num=10;
@Override
public void run() {
while (true){
if(num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在售卖第:"+num--+"号票");
}
}
}
}
public class SaleThread{
public static void main(String[] args) {
TicketThread t=new TicketThread();
Thread t1=new Thread(t,"窗口1");
Thread t2=new Thread(t,"窗口2");
Thread t3=new Thread(t,"窗口3");
Thread t4=new Thread(t,"窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
从最后几行输出结果可看出,这种现象是不应该存在的,说明多线程在售票时出现了安全问题。在售票程序的while循环中添加了sleep()方法,这样就模拟了售票过程的延迟。由于线程有延迟,当票号减为1时,假设窗口2线程出售1号票,对票号进行判断,进入while循环,在售票之前通过sleep()方法模拟售票时耗时操作,这时窗口1线程会进行售票,由于此时票号仍为1,因此窗口1线程也会进入循环,同理,4个线程都会进入循环,休眠结束后,4个线程都会进行售票,这样就将票号减了4次,结果出现了0,-1,-2等票号。
二(同步代码块)
线程安全问题是由多个线程同时处理共享资源所导致的,要想解决线程安全问题,就必须保证处理共享资源的代码在任意时刻只能有一个线程在执行。为此,Java提供了线程同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这段代码被称作“同步代码块”,语法格式如下:
synchronized(同步监视器){
// 需要被同步的代码
}
1.需要被同步的代码指的是,操作共享数据的代码
2.共享数据是:多个线程共同操作的变量
3. 同步监视器:俗称 锁.任何一个类的对象,都可以来当锁
要求:多个线程必须要公用一把锁
同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。“任意”说的是共享锁对象的类型。所以,锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,线程之间便不能产生同步的效果。
package 多线程;
class TicketThread1 implements Runnable{
int num=10;
@Override
public void run() {
while (true){
synchronized (this){
if(num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在售卖第:"+num--+"号票");
}
}
}
}
}
public class SaleThread1{
public static void main(String[] args) {
TicketThread1 t=new TicketThread1();
Thread t1=new Thread(t,"窗口1");
Thread t2=new Thread(t,"窗口2");
Thread t3=new Thread(t,"窗口3");
Thread t4=new Thread(t,"窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
从运行结果可看出,使用同步代码块后,不再出现票号为负数的情况,说明解决了线程安全性问题。
三(同步方法)
同样,在方法前面使用synchronized关键字来修饰,被修饰的方法称为“同步方法”,它能实现和同步代码块一样的功能,具体格式如下:
[修饰符] synchronized 返回值类型 方法名(方法参数){ 方法体}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会阻塞,指导当前的线程访问完毕后,其他线程才有机会执行。
package 多线程;
public class SaleThread2 {
public static void main(String[] args) {
TicketThread2 t=new TicketThread2();
Thread t1=new Thread(t,"窗口1");
Thread t2=new Thread(t,"窗口2");
Thread t3=new Thread(t,"窗口3");
Thread t4=new Thread(t,"窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class TicketThread2 implements Runnable{
int num=10;
@Override
public void run() {
while (true){
sale();
}
}
public synchronized void sale( ){
if(num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在售卖第:"+num--+"号票");
}
}
}
四(同步锁)
从JDK5开始,新增Lock锁方式解决线程安全问题。 Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只有一个线程对Lock对象加锁。通过Lock接口的实现类ReentrantLock来创建一个Lock锁对象,并通过Lock锁对象的lock()方法和unlock()对核心代码块进行上锁和释放锁。为了保证所有情况下都能正常解锁以确保其他线程可以执行,通常强狂下会在finally{ }代码块中调用unlock()方法来解锁。
lock():调用锁定方法,锁定当前线程
unlock():释放此锁,必须释放锁的
package 多线程;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockThread {
public static void main(String[] args) {
TicketThread3 t=new TicketThread3();
Thread t1=new Thread(t,"窗口1");
Thread t2=new Thread(t,"窗口2");
Thread t3=new Thread(t,"窗口3");
Thread t4=new Thread(t,"窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class TicketThread3 implements Runnable{
int num=10;
Lock lock=new ReentrantLock();//定义一个Lock锁对象
@Override
public void run() {
while(true){
lock.lock();//对代码块进行加锁
if (num>0){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"正在售卖第:"+num--+"号票");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//执行完代码后释放锁
}
}
else{
lock.unlock();//用完锁后当前线程,在此不考虑分布式的情况,及时的把锁给释放掉就行。
break;
}
}
}
}
五(死锁)
死锁: 不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
注意:出现死锁,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续操作,所以我们在使用同步的时候要避免死锁现象产生。
如何避免死锁产生:
1.尽量避免同步的嵌套
2.尽量避免同步资源的定义
3.专门的算法/原则
package 多线程;
public class ThreadDemo3 {
public static void main(String[] args) {
StringBuffer sb1=new StringBuffer();
StringBuffer sb2=new StringBuffer();
//分别做两个线程对sb1 和sb2追加
new Thread(){
@Override
public void run() {
synchronized (sb1){
sb1.append("a");
sb2.append("1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sb2){
sb1.append("b");
sb2.append("2");
System.out.println(this.getName()+sb1);
System.out.println(this.getName()+sb2);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (sb2){
sb1.append("c");
sb2.append("3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sb1){
sb1.append("d");
sb2.append("4");
System.out.println(this.getName()+sb1);
System.out.println(this.getName()+sb2);
}
}
}
}.start();;
}
}
案例:使用A、B、C3个线程输出,要求:A输出1-5,B输出6-10,C输出11-15。
package 多线程;
public class ThreadDemo4 {
public static void main(String[] args) {
ShowNum sn=new ShowNum();
Thread a=new Thread(sn,"A线程");
Thread b=new Thread(sn,"B线程");
Thread c=new Thread(sn,"C线程");
a.start();
b.start();
c.start();
}
}
class ShowNum implements Runnable{
int num=0;
@Override
public void run() {
while (true){
synchronized (this){
if(num< 15){
System.out.println(Thread.currentThread().getName()+"-----"+(++num));
notifyAll();
if(num%5==0){
//让出cpu还要释放锁
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}else {
break;
}
}
}
}
}