자바의 문자열은 불변이다. String의 함수를 호출을 하면 해당 객체를 직접 수정하는 것이 아니라, 함수의 결과로 해당 객체가 아닌 다른 객체를 반환한다. 그러나 항상 그런 것은 아니다.
대문자로 문자열 생성 후 String의 toUpperCase()를 호출하면 내부 구현에서 lower case의 문자가 발견되지 않으면 기존의 객체를 반환한다.
문자열변수.interen()을 하게되면
해당 문자열과 동일한 값을 가진 문자열을 상수풀에서 찾고 있다면 해당 문자열을 바라보게 하고 없다면 새로 생성하여 반환합니다.
언제 사용될까?
만약 문자열을 == 비교연산으로 비교해야 한다면. intern() 메서드가 사용될 수 있다. 해당 메서드에서 상수풀을 찾아 같은 값을 가지고 있다면 해당 참조를 반환해주기 때문에 속도가 더 빠를 수 있지만 해당 문자열이 상수풀에 없을때는 equals보다 느릴 수 도 있다. 상황에 따라 잘 사용해야할 것 같다!
OutOfMemory : JVM에서 설정된 메모리의 한계를 벗어난 상황일 때 발생. 힙 사이즈가 부족하거나 너무 많은 class를 로드할때, 가용가능한 swap이 없을때 큰 메모리의 native 메서드가 호출될때 등이 있다. 이를 해결하기 위해 dump파일분석, jvm 옵션 수정 등이 있다.
자바는 멀티스레드 환경에서 동기화를 지원하기 위해 가장 기초적인 장치인 ’고유 락(Intrinsic Lock)’을 지원한다. 개발자는 synchronized 키워드를 이용해서 특정 객체의 고유락을 사용해 여러 스레드를 동기화 시킬 수 있다.
Synchronized 블록은 Intrinsic Lock을 이용해서, Thread의 접근을 제어한다.
Java의 synchronized
동일한 객체에 대해 synchronized 블록을 사용하는 두 스레드는 한 번에 하나의 스레드만 내부로 들어갈 수 있고, 이 것이 자바가 제공하는 가장 기본적인 ‘상호배제(Mutual Exclusion)’ 장치이다.
synchronized에는 4가지의 사용법이 있다.
synchronized method
synchronized block
static synchronized method
static synchronized block
4가지 방식의 차이인 lock이 적용되는 범위를 알아보자
1. synchronized method
synchronized method는 클래스의 인스턴스에 대하여 lock을 건다.
하나의 인스턴스에 대하여 2개의 thread가 경합하는 상황
public class Main {
public static void main(String[] args) {
A a = new A();
Thread thread1 = new Thread(()->{
a.run("t1");
});
Thread thread2 = new Thread(()->{
a.run("t2");
});
thread1.start();
thread2.start();
}
}
public class A {
public synchronized void run(String name){
System.out.println(name + "lock");
try{
Thread.sleep(1000);
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(name + "unlock");
}
}
순서대로 lock을 획득하고 반납함
실행 결과
t1lock t1unlock t2lock t2unlock
각각의 인스턴스를 만들고 실행 해보자
public class Main {
public static void main(String[] args) {
A a = new A();
A a1 = new A();
Thread thread1 = new Thread(()->{
a.run("t1");
});
Thread thread2 = new Thread(()->{
a1.run("t2");
});
thread1.start();
thread2.start();
}
}
lock을 공유하지 않기 때문에 동기화가 발생하지 않음
실행 결과
t1lock t2lock t1unlock t2unlock
결과
synchronized method는 인스턴스에 대하여 lock을 건다.
인스턴스에 대해서 lock을 건다라는 표현이 인스턴스 접근 자체가 lock이 걸리는 걸까?? 확인해보자
public class Main {
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread thread1 = new Thread(()->{
a.run("t1");
});
Thread thread2 = new Thread(()->{
a.print("t2");
});
thread1.start();
Thread.sleep(500);
thread2.start();
}
}
public class A {
public void print(String name){
System.out.println(name + " hi");
}
public synchronized void run(String name){
System.out.println(name + "lock");
try{
Thread.sleep(1000);
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(name + "unlock");
}
}
synchronized가 적용되지 않은 print() 메서드를 추가하고 lock이 걸린 중간에 호출 해보았더니 run() 메서드의 lock과 상관없이 정상적으로 호출됨
static이 포함된 synchronized method방식은 우리가 일반적으로 생각하는 static 성질을 갖는다. 인스턴스가 아닌 클래스 단위로 lock이 발생한다.
public class Main {
public static void main(String[] args) throws InterruptedException {
A a1 = new A();
A a2 = new A();
Thread thread1 = new Thread(()->{
a1.run("t1");
});
Thread thread2 = new Thread(()->{
a2.run("t2");
});
thread1.start();
thread2.start();
}
}
public class A {
public static synchronized void run(String name){
System.out.println(name + "lock");
try{
Thread.sleep(1000);
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(name + "unlock");
}
}
실행 결과
t1lock t1unlock t2lock t2unlock
다른 인스턴스 이지만 클래스 단위로 lock이 발생했다.
만약 static synchronized method와 synchronized method가 섞여있다면 어떨까?
public class Main {
public static void main(String[] args) throws InterruptedException {
A a1 = new A();
A a2 = new A();
Thread thread1 = new Thread(()->{
a1.run("t1");
});
Thread thread2 = new Thread(()->{
a2.print("t2");
});
thread1.start();
thread2.start();
}
}
public class A {
public synchronized void print(String name){
System.out.println(name + " hi");
}
public static synchronized void run(String name){
System.out.println(name + "lock");
try{
Thread.sleep(1000);
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(name + "unlock");
}
}
실행 결과
t1lock t2 hi t1unlock
인스턴스 단위의 lock과 클래스 단위의 lock은 공유되지 않았다.
static synchronized method를 정리해보면
클래스 단위로 lock을 걸지만
인스턴스 단위의 synchronized method와 lock을 공유하지 않는다.
3. synchronized block
synchronized block은 인스턴스의 block단위로 lock을 건다. 이 때, lock 객체를 지정해줘야 한다.
public class Main {
public static void main(String[] args) throws InterruptedException {
A a1 = new A();
Thread thread1 = new Thread(()->{
a1.run("t1");
});
Thread thread2 = new Thread(()->{
a1.run("t2");
});
thread1.start();
thread2.start();
}
}
public class A {
public void run(String name) {
synchronized (this) {
System.out.println(name + "lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "unlock");
}
}
}
실행 결과
t1lock t1unlock t2lock t2unlock
block의 인자로 this를 주었다.
this는 해당 인스턴스를 의미하고 위 코드에서 method 전체가 block으로 감싸져 있으므로 메서드 선언부에 synchronized 키워드를 붙인 것과 똑같이 동작한다.
하지만 여러 로직이 섞여 있는 사이 부분만 lock을 걸 수 있다. lock은 synchronized block에 진입할 때 획득하고 빠져나오면서 반납하므로 block으로 범위를 지정하는 것이 효율적이다.
synchronized block도 method와 동일하게 인스턴스에 대해서 적용된다.
지금까지는 block에 인자로 this를 사용해서 Synchronized를 메서드 선언부에 붙인 것과 별반 다를 것 없이 사용함
이제는 block에 자원을 명시하고 사용
public class Main {
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread thread1 = new Thread(() -> {
a.run("thread1");
});
Thread thread2 = new Thread(() -> {
a.run("thread2");
});
Thread thread3 = new Thread(() -> {
a.print("자원 B와 상관 없는 thread3");
});
thread1.start();
thread2.start();
thread3.start();
}
}
public class A {
B b = new B();
public void run(String name) {
synchronized (b){
System.out.println(name + " lock");
b.run();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
public synchronized void print(String name) {
System.out.println(name + " hello");
}
}
public class B extends Thread{
@Override
public synchronized void run() {
System.out.println("B lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B unlock");
}
}
실행 결과
thread1 lock 자원 B와 상관 없는 thread3 hello B lock B unlock thread1 unlock thread2 lock B lock B unlock thread2 unlock
매우 복잡한 상황이다. lock 객체를 A클래스가 아닌 B클래스의 인스턴스로 사용하고 있다.
thread1, thread2는 b를 사용하는 method를 호출하고 있지만
thread3은 b와 상관없는 method이기 때문에 b의 lock과 상관없이 출력되었다.
즉 인스턴스 단위 lock과 B를 block한 lock은 공유되지 않고 별도로 관리되는 것을 확인할 수 있다.
이번에는 block에 인스턴스가 아니라 class를 명시해보았다.
public class Main {
public static void main(String[] args) {
A a = new A();
Thread thread1 = new Thread(() -> {
a.run("thread1");
});
Thread thread2 = new Thread(() -> {
a.run("thread2");
});
thread1.start();
thread2.start();
}
}
public class A {
B b = new B();
public void run(String name) {
synchronized (B.class){
System.out.println(name + " lock");
b.run();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
public synchronized void print(String name) {
System.out.println(name + " hello");
}
}
public class B extends Thread{
@Override
public synchronized void run() {
System.out.println("B lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B unlock");
}
}
실행 결과
thread1 lock B lock B unlock thread1 unlock thread2 lock B lock B unlock thread2 unlock
block의 인자로 인스턴스가 아닌 .class를 주었다.
출력 결과를 보면 lock을 공유하고 있는 것을 확인할 수 있다.
block에는 객체를 넘기면 인스턴스 단위로 lock을 걸고, .class형식으로 넘기면 클래스 단위의 lock을 건다.
4. static synchronized block
static method안에 synchronized block을 지정할 수 있다. static의 특성 상 this같이 현재 객체를 가르키는 표현을 사용할 수 없다. static synchronized method 방식과 차이는 lock 객체를 지정하고 block으로 범위를 한정지을 수 있다는 점이다. 이외에 클래스 단위로 lock을 공유한다는 점은 같다.
public class Main {
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
Thread thread1 = new Thread(() -> {
a1.run("thread1");
});
Thread thread2 = new Thread(() -> {
a2.run("thread2");
});
thread1.start();
thread2.start();
}
}
public class A {
public static void run(String name) {
synchronized (A.class){
System.out.println(name + " lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " unlock");
}
}
}
자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트 형태로 데이터를 변환하는 기술
각자 PC의 OS마다 서로 다른 가상 메모리 주소 공간을 갖기 때문에, Reference Type의 데이터들은 인스턴스를 전달 할 수 없다.
따라서, 이런 문제를 해결하기 위해선 주소값이 아닌 Byte형태로 직렬화된 객체 데이터를 전달해야 한다.
직렬화된 데이터들은 모두 기본형이 되고, 이는 파일 저장이나 네트워크 전송 시 파싱이 가능한 유의미한 데이터가 된다. 따라서 전송 및 저장이 가능한 데이터로 만들어 주는 것이 바로 직렬화이다.
직렬화 조건
자바에서는 간단히 java.io.Serializable 인터페이스 구현으로 직렬화/역직렬화가 가능하다.
직렬화 대상
인터페이스 상속 받은 객체,
Primitive 타입의 데이터
Primitive 타입이 아닌 Reference 타입처럼 주소값을 지닌 객체들은 바이트로 변환하기 위해 Serializable 인터페이스를 구현해야 한다.
직렬화 방법
java.io.ObjectOutputStream 객체를 이용한다.
Member member = new Member("홍길동", "hong@hong.com", 25);
byte[] serializedMember;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(member);
// serializedMember -> 직렬화된 member 객체
serializedMember = baos.toByteArray();
}
}
// 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
System.out.println(Base64.getEncoder().encodeToString(serializedMember));
}
역직렬화 조건
직렬화 대상이 된 객체의 클래스가 클래스 패스에 존재해야 하며 import 되어 있어야 한다.
중요한 점은 직렬화와 역직렬화를 진행하는 시스템이 서로 다를 수 있다는 것을 반드시 고려해야 한다.
💡 자바 직렬화 대상 객체는 동일한 serialVersionUID를 가지고 있어야 한다(필수는 아님)
자바의 직렬화 왜 사용될까?
CSV, JSON 프로토콜 버퍼 등은 시스템의 고유 특성과 상관없는 대부분의 시스템에서의 데이터 교환 시 많이 사용된다. 하지만 자바 직렬화 형태의 데이터 교환은 자바 시스템 간의 데이터 교환을 위해서 존재한다.
그렇다면 자바에서도 CSV, JSON을 사용하면 되지 자바 직렬화를 써야 되는 이유가 있을까?
정답은 없지만 목적에 따라 적절하게 써야한다.
직렬화의 장점
자바 시스템에서 개발에 최적화 되어 있다.
복잡한 데이터 구조의 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화가 가능하다.
데이터 타입이 자동으로 맞춰진다.
직렬화의 단점
변경에 취약하기 때문에 예외사항이 발생할 가능성이 높다.
다른 포맷에 비해서 용량이 크다.
자바 직렬화는 언제 어디서 사용될까?
서블릿 세션
서블릿 기반의 WAS들은 대부분 세션의 자바 직렬화를 지원하고 있다. 물론 단순히 세션을 서블릿 메모리 위에서 운용한다면 직렬화를 필요로 하지 않지만 파일로 저장하거나 세션 클러스터링, DB를 저장하는 옵션 등을 선택하게 되면 세션 자체가 직렬화 되어 저장되어 전달된다.
캐시
자바 시스템에서 퍼포먼스를 위해 캐시 라이브러리 시스템을 많이 이용하게 된다. 개발을 하다보면 상당수의 클래스가 만들어지게 된다 예를들어 DB를 조회한 후 가져온 데이터 객체 같은 경우 실시간 형태로 요구하는 데이터가 아니라면 메모리, 외부 저장소, 파일 등을 저장소를 이용해서 데이터 객체를 저장한 후 동일한 요청이 오면 DB를 다시 요청하는 것이 아니라 저장된 객체를 찾아서 응답하게 하는 형태를 보통 캐시를 사용한다고 한다.
이렇게 캐시할 부분을 직렬화하여 저장해서 사용한다. 자바 직렬화만을 이용해서 캐시를 저장하지는 않지만 가장 간편하기 때문에 많이 사용된다.